mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
Merge branch 'feat/fileSystem' into develop
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "0.3.0-alpha.8",
|
||||
"@blocksuite/editor": "0.3.0-alpha.8",
|
||||
"@blocksuite/blocks": "0.3.0-alpha.9",
|
||||
"@blocksuite/editor": "0.3.0-alpha.9",
|
||||
"@blocksuite/icons": "^2.0.0",
|
||||
"@blocksuite/store": "0.3.0-alpha.8",
|
||||
"@blocksuite/store": "0.3.0-alpha.9",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/server": "^11.10.0",
|
||||
|
||||
@@ -66,7 +66,9 @@ export const EditorModeSwitch = ({
|
||||
style = {},
|
||||
}: AnimateRadioProps) => {
|
||||
const { mode: themeMode } = useTheme();
|
||||
const { mode, setMode } = useEditor();
|
||||
const { mode, setMode, getPageMeta } = useEditor();
|
||||
const pageMeta = getPageMeta();
|
||||
|
||||
const modifyRadioItemStatus = (): RadioItemStatus => {
|
||||
return {
|
||||
left: isHover
|
||||
@@ -99,6 +101,7 @@ export const EditorModeSwitch = ({
|
||||
data-testid="editor-mode-switcher"
|
||||
shrink={!isHover}
|
||||
style={style}
|
||||
disabled={!!pageMeta?.trash}
|
||||
>
|
||||
<AnimateRadioItem
|
||||
isLeft={true}
|
||||
|
||||
@@ -5,41 +5,42 @@ import type { ItemStatus } from './type';
|
||||
|
||||
const ANIMATE_DURATION = 500;
|
||||
|
||||
export const StyledAnimateRadioContainer = styled('div')<{ shrink: boolean }>(
|
||||
({ shrink, theme }) => {
|
||||
const animateScaleStretch = keyframes`${toString(
|
||||
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const animateScaleShrink = keyframes(
|
||||
`${toString(
|
||||
spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' })
|
||||
)}`
|
||||
);
|
||||
const shrinkStyle = shrink
|
||||
? {
|
||||
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
|
||||
background: 'transparent',
|
||||
}
|
||||
: {
|
||||
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
|
||||
};
|
||||
export const StyledAnimateRadioContainer = styled('div')<{
|
||||
shrink: boolean;
|
||||
disabled: boolean;
|
||||
}>(({ shrink, theme, disabled }) => {
|
||||
const animateScaleStretch = keyframes`${toString(
|
||||
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const animateScaleShrink = keyframes(
|
||||
`${toString(
|
||||
spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' })
|
||||
)}`
|
||||
);
|
||||
const shrinkStyle = shrink
|
||||
? {
|
||||
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
|
||||
background: 'transparent',
|
||||
}
|
||||
: {
|
||||
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
|
||||
};
|
||||
|
||||
return {
|
||||
height: '36px',
|
||||
borderRadius: '18px',
|
||||
background: theme.colors.hoverBackground,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`,
|
||||
border: '1px solid transparent',
|
||||
return {
|
||||
height: '36px',
|
||||
borderRadius: '18px',
|
||||
background: disabled ? 'transparent' : theme.colors.hoverBackground,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`,
|
||||
border: '1px solid transparent',
|
||||
|
||||
...shrinkStyle,
|
||||
':hover': {
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
...(disabled ? { pointerEvents: 'none' } : shrinkStyle),
|
||||
':hover': {
|
||||
border: disabled ? '' : `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMiddleLine = styled('div')<{
|
||||
hidden: boolean;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
StyledFAQ,
|
||||
StyledIconWrapper,
|
||||
StyledFAQWrapper,
|
||||
StyledTransformIcon,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
|
||||
export const FAQ = () => {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const { mode } = useTheme();
|
||||
const { mode: editorMode } = useEditor();
|
||||
const { triggerShortcutsModal, triggerContactModal } = useModal();
|
||||
const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFAQ
|
||||
className=""
|
||||
onMouseEnter={() => {
|
||||
setShowContent(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<Grow in={showContent}>
|
||||
<StyledFAQWrapper>
|
||||
<Tooltip content="Contact Us" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerContactModal();
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
<Tooltip content="Keyboard Shortcuts" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerShortcutsModal();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
</StyledFAQWrapper>
|
||||
</Grow>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper
|
||||
data-testid="faq-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
>
|
||||
<HelpIcon />
|
||||
</StyledIconWrapper>
|
||||
<StyledTransformIcon in={showContent}>
|
||||
<CloseIcon />
|
||||
</StyledTransformIcon>
|
||||
</div>
|
||||
</StyledFAQ>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+12
-16
@@ -5,25 +5,21 @@ import {
|
||||
StyledTitle,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { Content } from '@/ui/layout';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import EditorModeSwitch from '@/components/editor-mode-switch';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
import QuickSearchButton from './quick-search-button';
|
||||
import Header from './header';
|
||||
|
||||
export const PageHeader = () => {
|
||||
export const EditorHeader = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
const { editor, onPropsUpdated } = useEditor();
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const { editor, onPropsUpdated, getPageMeta } = useEditor();
|
||||
|
||||
useEffect(() => {
|
||||
onPropsUpdated(editor => {
|
||||
setTitle(editor.model.title);
|
||||
setTitle(editor.model.title || 'Untitled');
|
||||
});
|
||||
}, [onPropsUpdated]);
|
||||
|
||||
@@ -31,14 +27,20 @@ export const PageHeader = () => {
|
||||
setTitle(editor?.model.title || 'Untitled');
|
||||
}, [editor]);
|
||||
|
||||
const pageMeta = getPageMeta();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
{title && (
|
||||
<StyledTitle
|
||||
onMouseEnter={() => {
|
||||
if (pageMeta?.trash) return;
|
||||
|
||||
setIsHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (pageMeta?.trash) return;
|
||||
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
@@ -53,13 +55,7 @@ export const PageHeader = () => {
|
||||
</StyledSwitchWrapper>
|
||||
<Content ellipsis={true}>{title}</Content>
|
||||
<StyledSearchArrowWrapper>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
<QuickSearchButton />
|
||||
</StyledSearchArrowWrapper>
|
||||
</StyledTitleWrapper>
|
||||
</StyledTitle>
|
||||
@@ -68,4 +64,4 @@ export const PageHeader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
export default EditorHeader;
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './header';
|
||||
export * from './page-header';
|
||||
export { StyledTitle as HeaderWrapper } from './styles';
|
||||
export * from './editor-header';
|
||||
export * from './page-list-header';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import Header from './header';
|
||||
import { StyledPageListTittleWrapper } from './styles';
|
||||
import QuickSearchButton from './quick-search-button';
|
||||
|
||||
export type PageListHeaderProps = PropsWithChildren<{
|
||||
icon?: ReactNode;
|
||||
}>;
|
||||
export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
|
||||
return (
|
||||
<Header>
|
||||
<StyledPageListTittleWrapper>
|
||||
{icon}
|
||||
{children}
|
||||
<QuickSearchButton style={{ marginLeft: '5px' }} />
|
||||
</StyledPageListTittleWrapper>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageListHeader;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { IconButton, IconButtonProps } from '@/ui/button';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
export const QuickSearchButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearchButton;
|
||||
@@ -110,3 +110,14 @@ export const StyledSearchArrowWrapper = styled.div(() => {
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.sm,
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
StyledIsland,
|
||||
StyledIconWrapper,
|
||||
StyledIslandWrapper,
|
||||
StyledTransformIcon,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
|
||||
export type IslandItemNames = 'contact' | 'shortcuts';
|
||||
// export type IslandShowMap = Record<IslandItemNames, boolean>;
|
||||
|
||||
export const HelpIsland = ({
|
||||
showList = ['contact', 'shortcuts'],
|
||||
}: {
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const { mode } = useTheme();
|
||||
const { mode: editorMode } = useEditor();
|
||||
const { triggerShortcutsModal, triggerContactModal } = useModal();
|
||||
const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
className=""
|
||||
onMouseEnter={() => {
|
||||
setShowContent(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<Grow in={showContent}>
|
||||
<StyledIslandWrapper>
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content="Contact Us" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerContactModal();
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip content="Keyboard Shortcuts" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerShortcutsModal();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledIslandWrapper>
|
||||
</Grow>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper isEdgelessDark={isEdgelessDark}>
|
||||
<HelpIcon />
|
||||
</StyledIconWrapper>
|
||||
<StyledTransformIcon in={showContent}>
|
||||
<CloseIcon />
|
||||
</StyledTransformIcon>
|
||||
</div>
|
||||
</StyledIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpIsland;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
|
||||
export const StyledFAQ = styled('div')(({ theme }) => {
|
||||
export const StyledIsland = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -57,7 +57,7 @@ export const StyledIconWrapper = styled('div')<{ isEdgelessDark: boolean }>(
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledFAQWrapper = styled('div')(({ theme }) => {
|
||||
export const StyledIslandWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
@@ -9,16 +9,17 @@ dayjs.extend(localizedFormat);
|
||||
export const DateCell = ({
|
||||
pageMeta,
|
||||
dateKey,
|
||||
backupKey = '',
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
dateKey: keyof PageMeta;
|
||||
backupKey?: keyof PageMeta;
|
||||
}) => {
|
||||
// dayjs().format('L LT');
|
||||
const value = pageMeta[dateKey] ?? pageMeta[backupKey];
|
||||
return (
|
||||
<TableCell ellipsis={true}>
|
||||
{pageMeta[dateKey] === undefined
|
||||
? '--'
|
||||
: dayjs(pageMeta[dateKey] as string).format('YYYY-MM-DD HH:mm')}
|
||||
{value ? dayjs(value as string).format('YYYY-MM-DD HH:mm') : '--'}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,6 +85,7 @@ export const PageList = ({
|
||||
<DateCell
|
||||
pageMeta={pageMeta}
|
||||
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
|
||||
backupKey={isTrash ? 'trashDate' : 'createDate'}
|
||||
/>
|
||||
<TableCell style={{ padding: 0 }}>
|
||||
{isTrash ? (
|
||||
|
||||
@@ -40,9 +40,8 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete',
|
||||
content:
|
||||
'Deleted items will be moved to Trash Bin. Do you confirm?',
|
||||
title: 'Delete page?',
|
||||
content: `${pageMeta.title} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright: https://github.com/toeverything/blocksuite/commit/8032ef3ab97aefce01664b36502fc392c5db8b78#diff-bf5b41be21936f9165a8400c7f20e24d3dbc49644ba57b9258e0943f0dc1c464
|
||||
import { css, html, TemplateResult } from 'lit';
|
||||
|
||||
export const sleep = (ms = 0) =>
|
||||
new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
let ToastContainer: HTMLDivElement | null = null;
|
||||
|
||||
/**
|
||||
* DO NOT USE FOR USER INPUT
|
||||
* See https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
|
||||
*/
|
||||
const htmlToElement = <T extends ChildNode>(html: string | TemplateResult) => {
|
||||
const template = document.createElement('template');
|
||||
if (typeof html === 'string') {
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
template.innerHTML = html;
|
||||
} else {
|
||||
const { strings, values } = html;
|
||||
const v = [...values, '']; // + last emtpty part
|
||||
template.innerHTML = strings.reduce((acc, cur, i) => acc + cur + v[i], '');
|
||||
}
|
||||
return template.content.firstChild as T;
|
||||
};
|
||||
|
||||
const createToastContainer = () => {
|
||||
const styles = css`
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 78px;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
`;
|
||||
const template = html`<div style="${styles}"></div>`;
|
||||
const element = htmlToElement<HTMLDivElement>(template);
|
||||
document.body.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```ts
|
||||
* toast('Hello World');
|
||||
* ```
|
||||
*/
|
||||
export const toast = (message: string, duration = 2500) => {
|
||||
if (!ToastContainer) {
|
||||
ToastContainer = createToastContainer();
|
||||
}
|
||||
|
||||
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-tooltip-color);
|
||||
background: var(--affine-tooltip-background);
|
||||
box-shadow: var(--affine-tooltip-shadow);
|
||||
border-radius: 10px;
|
||||
transition: all 230ms cubic-bezier(0.21, 1.02, 0.73, 1);
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
const template = html`<div style="${styles}"></div>`;
|
||||
const element = htmlToElement<HTMLDivElement>(template);
|
||||
// message is not trusted
|
||||
element.textContent = message;
|
||||
ToastContainer.appendChild(element);
|
||||
|
||||
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(async () => {
|
||||
const fadeOut = fadeIn.reverse();
|
||||
const animation = element.animate(fadeOut, options);
|
||||
await animation.finished;
|
||||
element.style.maxHeight = '0';
|
||||
element.style.margin = '0';
|
||||
element.style.padding = '0';
|
||||
// wait for transition
|
||||
await sleep(230);
|
||||
element.remove();
|
||||
}, duration);
|
||||
return element;
|
||||
};
|
||||
|
||||
export default toast;
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
AddIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import Link from 'next/link';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
import { IconButton } from '@/ui/button';
|
||||
import useLocalStorage from '@/hooks/use-local-storage';
|
||||
|
||||
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
const { pageList, openPage } = useEditor();
|
||||
@@ -45,7 +47,7 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
openPage(pageMeta.id);
|
||||
}}
|
||||
>
|
||||
{pageMeta.title || pageMeta.id}
|
||||
{pageMeta.title || 'Untitled'}
|
||||
</StyledSubListItem>
|
||||
);
|
||||
})}
|
||||
@@ -57,11 +59,13 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
};
|
||||
export const WorkSpaceSliderBar = () => {
|
||||
const { triggerQuickSearchModal, triggerImportModal } = useModal();
|
||||
const [show, setShow] = useState(false);
|
||||
const [showSubFavorite, setShowSubFavorite] = useState(true);
|
||||
const { createPage, getPageMeta, openPage } = useEditor();
|
||||
const router = useRouter();
|
||||
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
const [show, setShow] = useLocalStorage('AFFINE_SLIDE_BAR', false, true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSliderBar show={show}>
|
||||
@@ -120,14 +124,27 @@ export const WorkSpaceSliderBar = () => {
|
||||
<AddIcon /> New Page
|
||||
</StyledNewPageButton>
|
||||
</StyledSliderBar>
|
||||
<StyledArrowButton
|
||||
isShow={show}
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
}}
|
||||
<Tooltip
|
||||
content={show ? 'Collapse sidebar' : 'Expand sidebar'}
|
||||
placement="right"
|
||||
visible={showTip}
|
||||
>
|
||||
<Arrow />
|
||||
</StyledArrowButton>
|
||||
<StyledArrowButton
|
||||
isShow={show}
|
||||
onClick={() => {
|
||||
setShow(!show);
|
||||
setShowTip(false);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShowTip(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowTip(false);
|
||||
}}
|
||||
>
|
||||
<Arrow />
|
||||
</StyledArrowButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
|
||||
export type UseLocalStorage = <S>(
|
||||
key: string,
|
||||
initialState: S | (() => S),
|
||||
// If initialState is different from the initial local data, set it
|
||||
initialValue?: S
|
||||
) => [S, Dispatch<SetStateAction<S>>];
|
||||
|
||||
export const useLocalStorage: UseLocalStorage = (
|
||||
key,
|
||||
defaultValue,
|
||||
initialValue
|
||||
) => {
|
||||
const [item, setItem] = useState(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
setItem(JSON.parse(saved));
|
||||
} else if (initialValue) {
|
||||
setItem(initialValue);
|
||||
}
|
||||
}, [initialValue, key, setItem]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
}, [item, key]);
|
||||
|
||||
return [item, setItem];
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
@@ -13,6 +13,9 @@ import '../utils/print-build-info';
|
||||
import { styled } from '@/styles';
|
||||
import ProviderComposer from '@/components/provider-composer';
|
||||
import ConfirmProvider from '@/providers/confirm-provider';
|
||||
import HelpIsland from '@/components/help-island';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ThemeProvider = dynamic(() => import('@/providers/themeProvider'), {
|
||||
ssr: false,
|
||||
});
|
||||
@@ -35,6 +38,8 @@ const StyledWrapper = styled('div')(({ theme }) => {
|
||||
});
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Logger />
|
||||
@@ -50,6 +55,9 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
<WorkSpaceSliderBar />
|
||||
<StyledWrapper>
|
||||
<Component {...pageProps} />
|
||||
<HelpIsland
|
||||
showList={router.pathname !== '/' ? ['contact'] : undefined}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</StyledPage>
|
||||
</ProviderComposer>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Modal from '@/ui/modal';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/button';
|
||||
import { FavouritedIcon } from '@blocksuite/icons';
|
||||
import { toast } from '@/components/toast';
|
||||
export const StyledHeader = styled('div')({
|
||||
height: '60px',
|
||||
width: '100vw',
|
||||
@@ -38,7 +39,14 @@ const Affine = () => {
|
||||
</Modal>
|
||||
<Loading />
|
||||
|
||||
<Button icon={<FavouritedIcon />}>click me!</Button>
|
||||
<Button
|
||||
icon={<FavouritedIcon />}
|
||||
onClick={() => {
|
||||
toast('hello, world!!');
|
||||
}}
|
||||
>
|
||||
click me!
|
||||
</Button>
|
||||
<Button icon={<FavouritedIcon />} type={'primary'}>
|
||||
click me!
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextPage } from 'next';
|
||||
import { styled } from '@/styles';
|
||||
import { PageHeader } from '@/components/header';
|
||||
import { FAQ } from '@/components/faq';
|
||||
import { EditorHeader } from '@/components/header';
|
||||
import EdgelessToolbar from '@/components/edgeless-toolbar';
|
||||
import MobileModal from '@/components/mobile-modal';
|
||||
import Editor from '@/components/editor';
|
||||
@@ -15,12 +14,11 @@ const StyledEditorContainer = styled('div')(({ theme }) => {
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
<EditorHeader />
|
||||
<MobileModal />
|
||||
<StyledEditorContainer>
|
||||
<Editor />
|
||||
</StyledEditorContainer>
|
||||
<FAQ />
|
||||
<EdgelessToolbar />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import { Header } from '@/components/header';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { PageList } from '@/components/page-list';
|
||||
import { AllPagesIcon } from '@blocksuite/icons';
|
||||
|
||||
import { HeaderWrapper } from '@/components/header';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledWrapper = styled(HeaderWrapper)(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.sm,
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
import { PageListHeader } from '@/components/header';
|
||||
|
||||
export const All = () => {
|
||||
const { pageList: allPageList } = useEditor();
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<StyledWrapper>
|
||||
<AllPagesIcon />
|
||||
All Pages
|
||||
</StyledWrapper>
|
||||
</Header>
|
||||
<PageListHeader icon={<AllPagesIcon />}>All Page</PageListHeader>
|
||||
<PageList
|
||||
pageList={allPageList.filter(p => !p.trash)}
|
||||
showFavoriteTag={true}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { Header } from '@/components/header';
|
||||
import { PageListHeader } from '@/components/header';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { PageList } from '@/components/page-list';
|
||||
import { StyledWrapper } from './all';
|
||||
import { FavouritesIcon } from '@blocksuite/icons';
|
||||
|
||||
export const Favorite = () => {
|
||||
const { pageList: allPageList } = useEditor();
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<StyledWrapper>
|
||||
<FavouritesIcon />
|
||||
Favorites
|
||||
</StyledWrapper>
|
||||
</Header>
|
||||
<PageListHeader icon={<FavouritesIcon />}>Favorites</PageListHeader>
|
||||
<PageList pageList={allPageList.filter(p => p.favorite && !p.trash)} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { Header } from '@/components/header';
|
||||
import { PageListHeader } from '@/components/header';
|
||||
import { useEditor } from '@/providers/editor-provider';
|
||||
import { PageList } from '@/components/page-list';
|
||||
import { StyledWrapper } from './all';
|
||||
import { TrashIcon } from '@blocksuite/icons';
|
||||
export const Trash = () => {
|
||||
const { pageList: allPageList } = useEditor();
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<StyledWrapper>
|
||||
<TrashIcon />
|
||||
Trash
|
||||
</StyledWrapper>
|
||||
</Header>
|
||||
<PageListHeader icon={<TrashIcon />}>Trash</PageListHeader>
|
||||
<PageList pageList={allPageList.filter(p => p.trash)} isTrash={true} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EditorProvider = ({
|
||||
|
||||
const { mode, setMode } = useMode({ workspace, page });
|
||||
|
||||
const editorHandlers = useEditorHandler({ workspace, editor });
|
||||
const editorHandlers = useEditorHandler({ workspace, editor, page });
|
||||
const onPropsUpdated = usePropsUpdated(editor);
|
||||
const onHistoryUpdated = useHistoryUpdated(page);
|
||||
// Modify the updatedDate when history change
|
||||
|
||||
@@ -113,6 +113,11 @@ const EditorReactor = ({
|
||||
editor.mode = pageMeta.mode;
|
||||
}
|
||||
|
||||
if (pageMeta?.trash) {
|
||||
// @ts-ignore
|
||||
editor.readonly = true;
|
||||
}
|
||||
|
||||
setEditor(editor);
|
||||
}, [workspace, currentPage, setEditor]);
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { QueryContent } from '@blocksuite/store/dist/workspace/search';
|
||||
import { createPage, initialPage, generateDefaultPageId } from '../utils';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { Workspace, Page } from '@blocksuite/store';
|
||||
import { useRouter } from 'next/router';
|
||||
import { EditorHandlers, PageMeta } from '../interface';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
|
||||
import toast from '@/components/toast';
|
||||
export const useEditorHandler = ({
|
||||
editor,
|
||||
workspace,
|
||||
page,
|
||||
}: {
|
||||
workspace?: Workspace;
|
||||
page?: Page;
|
||||
editor?: EditorContainer;
|
||||
}): EditorHandlers => {
|
||||
const router = useRouter();
|
||||
@@ -20,8 +22,8 @@ export const useEditorHandler = ({
|
||||
initialPage(page, title);
|
||||
return page;
|
||||
},
|
||||
getPageMeta(pageId: string) {
|
||||
pageId = pageId.replace('space:', '');
|
||||
getPageMeta(pageId) {
|
||||
pageId = (pageId ?? page!.id).replace('space:', '');
|
||||
return workspace!.meta.pageMetas.find(
|
||||
page => page.id === pageId
|
||||
) as PageMeta;
|
||||
@@ -42,22 +44,26 @@ export const useEditorHandler = ({
|
||||
trash: !pageMeta.trash,
|
||||
trashDate: +new Date(),
|
||||
});
|
||||
|
||||
if (pageMeta.trash) {
|
||||
toast('Removed to trash');
|
||||
const editor = document.querySelector('editor-container');
|
||||
// @ts-ignore
|
||||
editor?.setAttribute('readonly', 'false');
|
||||
}
|
||||
}
|
||||
},
|
||||
favoritePage: pageId => {
|
||||
workspace!.setPageMeta(pageId, { favorite: true });
|
||||
},
|
||||
permanentlyDeletePage: pageId => {
|
||||
// TODO: workspace.meta.removePage or workspace.removePage?
|
||||
workspace!.meta.removePage(pageId);
|
||||
},
|
||||
unFavoritePage: pageId => {
|
||||
workspace!.setPageMeta(pageId, { favorite: true });
|
||||
},
|
||||
toggleFavoritePage: pageId => {
|
||||
const pageMeta = workspace?.meta.pageMetas.find(p => p.id === pageId);
|
||||
if (pageMeta) {
|
||||
workspace!.setPageMeta(pageId, { favorite: !pageMeta.favorite });
|
||||
toast(
|
||||
pageMeta.favorite ? 'Removed to Favourites' : 'Added to Favourites'
|
||||
);
|
||||
}
|
||||
},
|
||||
search: (query: QueryContent) => {
|
||||
|
||||
@@ -31,10 +31,8 @@ export type EditorHandlers = {
|
||||
pageId: string,
|
||||
query?: { [key: string]: string }
|
||||
) => Promise<boolean>;
|
||||
getPageMeta: (pageId: string) => PageMeta | void;
|
||||
getPageMeta: (pageId?: string) => PageMeta | void;
|
||||
toggleDeletePage: (pageId: string) => void;
|
||||
favoritePage: (pageId: string) => void;
|
||||
unFavoritePage: (pageId: string) => void;
|
||||
toggleFavoritePage: (pageId: string) => void;
|
||||
permanentlyDeletePage: (pageId: string) => void;
|
||||
search: (query: QueryContent) => Map<string, string | undefined>;
|
||||
|
||||
@@ -16,6 +16,7 @@ export const getLightTheme = (
|
||||
|
||||
pageBackground: '#fff',
|
||||
hoverBackground: '#F1F3FF',
|
||||
innerHoverBackground: '#E0E6FF',
|
||||
popoverBackground: '#fff',
|
||||
tooltipBackground: '#6880FF',
|
||||
codeBackground: '#f2f5f9',
|
||||
@@ -24,7 +25,7 @@ export const getLightTheme = (
|
||||
|
||||
textColor: '#3A4C5C',
|
||||
edgelessTextColor: '#3A4C5C',
|
||||
iconColor: '#9096A5',
|
||||
iconColor: '#888a9e',
|
||||
linkColor: '#6880FF',
|
||||
linkColor2: '#6880FF',
|
||||
linkVisitedColor: '#ABB8FE',
|
||||
@@ -87,6 +88,7 @@ export const getDarkTheme = (
|
||||
|
||||
pageBackground: '#2c2c2c',
|
||||
hoverBackground: '#3C3C42',
|
||||
innerHoverBackground: '#E0E6FF',
|
||||
popoverBackground: '#1F2021',
|
||||
tooltipBackground: '#1F2021',
|
||||
codeBackground:
|
||||
@@ -98,7 +100,7 @@ export const getDarkTheme = (
|
||||
|
||||
textColor: '#fff',
|
||||
edgelessTextColor: '#3A4C5C',
|
||||
iconColor: '#9096A5',
|
||||
iconColor: '#888a9e',
|
||||
linkColor: '#7D91FF',
|
||||
linkColor2: '#6880FF',
|
||||
linkVisitedColor: '#505FAB',
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface AffineTheme {
|
||||
popoverBackground: string;
|
||||
tooltipBackground: string;
|
||||
hoverBackground: string;
|
||||
innerHoverBackground: string;
|
||||
codeBackground: string;
|
||||
warningBackground: string;
|
||||
errorBackground: string;
|
||||
|
||||
@@ -4,8 +4,7 @@ import { ModalWrapper } from '@/ui/modal';
|
||||
export const StyledModalWrapper = styled(ModalWrapper)(({ theme }) => {
|
||||
return {
|
||||
width: '460px',
|
||||
height: '240px',
|
||||
padding: '0 60px',
|
||||
padding: '46px 60px 32px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,7 +13,6 @@ export const StyledConfirmTitle = styled.div(({ theme }) => {
|
||||
fontSize: theme.font.h6,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
marginTop: '45px',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
Generated
+15
-15
@@ -22,10 +22,10 @@ importers:
|
||||
|
||||
packages/app:
|
||||
specifiers:
|
||||
'@blocksuite/blocks': 0.3.0-alpha.8
|
||||
'@blocksuite/editor': 0.3.0-alpha.8
|
||||
'@blocksuite/blocks': 0.3.0-alpha.9
|
||||
'@blocksuite/editor': 0.3.0-alpha.9
|
||||
'@blocksuite/icons': ^2.0.0
|
||||
'@blocksuite/store': 0.3.0-alpha.8
|
||||
'@blocksuite/store': 0.3.0-alpha.9
|
||||
'@emotion/css': ^11.10.0
|
||||
'@emotion/react': ^11.10.4
|
||||
'@emotion/server': ^11.10.0
|
||||
@@ -55,10 +55,10 @@ importers:
|
||||
react-dom: 18.2.0
|
||||
typescript: 4.8.3
|
||||
dependencies:
|
||||
'@blocksuite/blocks': 0.3.0-alpha.8
|
||||
'@blocksuite/editor': 0.3.0-alpha.8
|
||||
'@blocksuite/blocks': 0.3.0-alpha.9
|
||||
'@blocksuite/editor': 0.3.0-alpha.9
|
||||
'@blocksuite/icons': 2.0.0_w5j4k42lgipnm43s3brx6h3c34
|
||||
'@blocksuite/store': 0.3.0-alpha.8
|
||||
'@blocksuite/store': 0.3.0-alpha.9
|
||||
'@emotion/css': 11.10.0
|
||||
'@emotion/react': 11.10.4_w5j4k42lgipnm43s3brx6h3c34
|
||||
'@emotion/server': 11.10.0_@emotion+css@11.10.0
|
||||
@@ -176,10 +176,10 @@ packages:
|
||||
to-fast-properties: 2.0.0
|
||||
dev: false
|
||||
|
||||
/@blocksuite/blocks/0.3.0-alpha.8:
|
||||
resolution: {integrity: sha512-Q7md4N1H0lTMG0nX1PnAdSlTjBfNX4nFLlcX3SrdgjSM9QOu0G6E6OvfHHHCj4SJPnzyHH5xb6Lb0jU9Lpas6Q==}
|
||||
/@blocksuite/blocks/0.3.0-alpha.9:
|
||||
resolution: {integrity: sha512-7Zw2lL5DZ+JB6dtmGKfuW19nG5gMvLPcTakK9oMszbhgETnwHUUN382Pz3urecuDA88yaLj8qbC1VdbAQWDW5w==}
|
||||
dependencies:
|
||||
'@blocksuite/store': 0.3.0-alpha.8
|
||||
'@blocksuite/store': 0.3.0-alpha.9
|
||||
'@tldraw/intersect': 1.8.0
|
||||
'@tldraw/vec': 1.8.0
|
||||
hotkeys-js: 3.10.0
|
||||
@@ -193,11 +193,11 @@ packages:
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@blocksuite/editor/0.3.0-alpha.8:
|
||||
resolution: {integrity: sha512-Sx5E69LMZkUcc37q2PLXAnNTHy9okn/UFBfcWG8i5LO/T8QpYJgbllZGJDx7A3qP8Uz+BQyhgI5gp8S/dnra7g==}
|
||||
/@blocksuite/editor/0.3.0-alpha.9:
|
||||
resolution: {integrity: sha512-dnrk87akzwRRKxS18dec2rY2ZvfIpvNq9SIHEPCCk2UNLI2R66HUO5v1RhLi6/oglbWtizcqzQWCa/DjjY1ljA==}
|
||||
dependencies:
|
||||
'@blocksuite/blocks': 0.3.0-alpha.8
|
||||
'@blocksuite/store': 0.3.0-alpha.8
|
||||
'@blocksuite/blocks': 0.3.0-alpha.9
|
||||
'@blocksuite/store': 0.3.0-alpha.9
|
||||
lit: 2.4.0
|
||||
marked: 4.1.1
|
||||
turndown: 7.1.1
|
||||
@@ -217,8 +217,8 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@blocksuite/store/0.3.0-alpha.8:
|
||||
resolution: {integrity: sha512-u63FWZSiYID8++tEIaQZ5FSj7NZu9XM2ZdTaTU+7PDEwucK5Gqo8+YrEC9gVl+2jkSW5cp43pxf2kZolUgkyYg==}
|
||||
/@blocksuite/store/0.3.0-alpha.9:
|
||||
resolution: {integrity: sha512-5wdnE6JUL1/y/LufqcRg2lRL5mYvdXc+MOj98eNfFAv+JFbY4vuGZ8N9HFn6dQ/rQaQsgaoAZkWyogbbC8x4IA==}
|
||||
dependencies:
|
||||
buffer: 6.0.3
|
||||
flexsearch: 0.7.21
|
||||
|
||||
Reference in New Issue
Block a user