refactor!: remove next.js (#3267)

This commit is contained in:
Alex Yang
2023-07-19 00:53:10 +08:00
committed by GitHub
parent 79227a1e7c
commit 47f12f77f2
296 changed files with 4115 additions and 3617 deletions
@@ -0,0 +1,2 @@
export type { EditorProps } from '@affine/component/block-suite-editor';
export { BlockSuiteEditor } from '@affine/component/block-suite-editor';
@@ -0,0 +1,27 @@
import { style } from '@vanilla-extract/css';
export const pageListEmptyStyle = style({
height: 'calc(100% - 52px)',
});
export const emptyDescButton = style({
cursor: 'pointer',
color: 'var(--affine-text-secondary-color)',
background: 'var(--affine-background-code-block)',
border: '1px solid var(--affine-border-color)',
borderRadius: '4px',
padding: '0 6px',
boxSizing: 'border-box',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
},
});
export const emptyDescKbd = style([
emptyDescButton,
{
cursor: 'text',
},
]);
@@ -0,0 +1,243 @@
import { Empty } from '@affine/component';
import type { ListData, TrashListData } from '@affine/component/page-list';
import { PageList, PageListTrashView } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { getPagePreviewText } from '@toeverything/hooks/use-block-suite-page-preview';
import { useAtom } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { filterPage } from '../../../utils/filter';
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
import { usePageHelper } from './utils';
export type BlockSuitePageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
listType: 'all' | 'trash' | 'shared' | 'public';
isPublic?: true;
onOpenPage: (pageId: string, newTab?: boolean) => void;
collection?: Collection;
};
const filter = {
all: (pageMeta: PageMeta) => !pageMeta.trash,
public: (pageMeta: PageMeta) => !pageMeta.trash,
trash: (pageMeta: PageMeta, allMetas: PageMeta[]) => {
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
return !parentMeta?.trash && pageMeta.trash;
},
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
const PageListEmpty = (props: {
createPage?: () => void;
listType: BlockSuitePageListProps['listType'];
}) => {
const { listType, createPage } = props;
const t = useAFFiNEI18N();
const getEmptyDescription = () => {
if (listType === 'all') {
const CreateNewPageButton = () => (
<button className={emptyDescButton} onClick={createPage}>
New Page
</button>
);
if (environment.isDesktop) {
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
return (
<Trans i18nKey="emptyAllPagesClient">
Click on the <CreateNewPageButton /> button Or press
<kbd className={emptyDescKbd}>{{ shortcut } as any}</kbd> to create
your first page.
</Trans>
);
}
return (
<Trans i18nKey="emptyAllPages">
Click on the
<CreateNewPageButton />
button to create your first page.
</Trans>
);
}
if (listType === 'trash') {
return t['emptyTrash']();
}
if (listType === 'shared') {
return t['emptySharedPages']();
}
return;
};
return (
<div className={pageListEmptyStyle}>
<Empty
title={t['com.affine.emptyDesc']()}
description={getEmptyDescription()}
/>
</div>
);
};
export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
blockSuiteWorkspace,
onOpenPage,
listType,
isPublic = false,
collection,
}) => {
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const {
toggleFavorite,
removeToTrash,
restoreFromTrash,
permanentlyDeletePage,
cancelPublicPage,
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const [filterMode] = useAtom(allPageModeSelectAtom);
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const getPageInfo = useGetPageInfoById(blockSuiteWorkspace);
const tagOptionMap = useMemo(
() =>
Object.fromEntries(
blockSuiteWorkspace.meta.properties.tags.options.map(v => [v.id, v])
),
[blockSuiteWorkspace.meta.properties.tags.options]
);
const list = useMemo(
() =>
pageMetas
.filter(pageMeta => {
if (filterMode === 'all') {
return true;
}
if (filterMode === 'edgeless') {
return isPreferredEdgeless(pageMeta.id);
}
if (filterMode === 'page') {
return !isPreferredEdgeless(pageMeta.id);
}
console.error('unknown filter mode', pageMeta, filterMode);
return true;
})
.filter(pageMeta => {
if (!filter[listType](pageMeta, pageMetas)) {
return false;
}
if (!collection) {
return true;
}
return filterPage(collection, pageMeta);
}),
[pageMetas, filterMode, isPreferredEdgeless, listType, collection]
);
if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => {
const page = blockSuiteWorkspace.getPage(pageMeta.id);
const preview = page ? getPagePreviewText(page) : undefined;
return {
icon: isPreferredEdgeless(pageMeta.id) ? (
<EdgelessIcon />
) : (
<PageIcon />
),
pageId: pageMeta.id,
title: pageMeta.title,
preview,
createDate: new Date(pageMeta.createDate),
trashDate: pageMeta.trashDate
? new Date(pageMeta.trashDate)
: undefined,
onClickPage: () => onOpenPage(pageMeta.id),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(t['restored']({ title: pageMeta.title || 'Untitled' }));
},
onPermanentlyDeletePage: () => {
permanentlyDeletePage(pageMeta.id);
toast(t['Permanently deleted']());
},
};
});
return (
<PageListTrashView
list={pageList}
fallback={<PageListEmpty listType={listType} />}
/>
);
}
const pageList: ListData[] = list.map(pageMeta => {
const page = blockSuiteWorkspace.getPage(pageMeta.id);
const preview = page ? getPagePreviewText(page) : undefined;
return {
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
preview,
tags:
page?.meta.tags?.map(id => tagOptionMap[id]).filter(v => v != null) ??
[],
favorite: !!pageMeta.favorite,
isPublicPage: !!pageMeta.isPublic,
createDate: new Date(pageMeta.createDate),
updatedDate: new Date(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id),
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
removeToTrash: () => {
removeToTrash(pageMeta.id);
toast(t['Successfully deleted']());
},
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(t['restored']({ title: pageMeta.title || 'Untitled' }));
},
bookmarkPage: () => {
const status = pageMeta.favorite;
toggleFavorite(pageMeta.id);
toast(
status ? t['Removed from Favorites']() : t['Added to Favorites']()
);
},
onDisablePublicSharing: () => {
cancelPublicPage(pageMeta.id);
toast('Successfully disabled', {
portal: document.body,
});
},
};
});
return (
<PageList
workspaceId={blockSuiteWorkspace.id}
propertiesMeta={blockSuiteWorkspace.meta.properties}
getPageInfo={getPageInfo}
onCreateNewPage={createPage}
onCreateNewEdgeless={createEdgeless}
onImportFile={importFile}
isPublicWorkspace={isPublic}
list={pageList}
fallback={<PageListEmpty createPage={createPage} listType={listType} />}
/>
);
};
@@ -0,0 +1,37 @@
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { pageSettingsAtom, setPageModeAtom } from '../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const { openPage } = useNavigateHelper();
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const pageSettings = useAtomValue(pageSettingsAtom);
const isPreferredEdgeless = useCallback(
(pageId: string) => pageSettings[pageId]?.mode === 'edgeless',
[pageSettings]
);
const setPageMode = useSetAtom(setPageModeAtom);
const createPageAndOpen = useCallback(() => {
const page = createPage();
return openPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace.id, createPage, openPage]);
const createEdgelessAndOpen = useCallback(() => {
const page = createPage();
setPageMode(page.id, 'edgeless');
return openPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace.id, createPage, openPage, setPageMode]);
const importFileAndOpen = useCallback(async () => {
const { showImportModal } = await import('@blocksuite/blocks');
showImportModal({ workspace: blockSuiteWorkspace });
}, [blockSuiteWorkspace]);
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,
importFile: importFileAndOpen,
isPreferredEdgeless: isPreferredEdgeless,
};
};
@@ -0,0 +1,27 @@
import { DownloadTips } from '@affine/component/affine-banner';
import { isDesktop } from '@affine/env/constant';
export const DownloadClientTip = ({
show,
onClose,
}: {
// const [showDownloadClientTips, setShowDownloadClientTips] = useAtom(
// guideDownloadClientTipAtom
// );
// const onCloseDownloadClient = useCallback(() => {
// setShowDownloadClientTips(false);
// }, [setShowDownloadClientTips]);
// if (!showDownloadClientTips || isDesktop) {
// return <></>;
// }
show: boolean;
onClose: () => void;
}) => {
if (!show || isDesktop) {
return null;
}
return <DownloadTips onClose={onClose} />;
};
export default DownloadClientTip;
@@ -0,0 +1,669 @@
{
"v": "5.9.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 60.0000024438501,
"w": 500,
"h": 500,
"nm": "edgeless-hover",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Layer 3/paper-edgeless-icons Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [250, 250, 0], "ix": 2, "l": 2 },
"a": { "a": 0, "k": [183.5, 183.5, 0], "ix": 1, "l": 2 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, -5.604],
[0, 0],
[-5.614, 0],
[0, 0],
[0, 5.605],
[0, 0],
[5.615, 0]
],
"o": [
[-5.614, 0],
[0, 0],
[0, 5.605],
[0, 0],
[5.615, 0],
[0, 0],
[0, -5.604],
[0, 0]
],
"v": [
[-30.525, -40.7],
[-40.699, -30.525],
[-40.699, 30.524],
[-30.525, 40.699],
[30.525, 40.699],
[40.699, 30.524],
[40.699, -30.525],
[30.525, -40.7]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[22.447, 0],
[0, 0],
[0, 22.437],
[0, 0],
[-22.446, 0],
[0, 0],
[0, -22.437],
[0, 0]
],
"o": [
[0, 0],
[-22.446, 0],
[0, 0],
[0, -22.437],
[0, 0],
[22.447, 0],
[0, 0],
[0, 22.437]
],
"v": [
[30.525, 71.224],
[-30.525, 71.224],
[-71.225, 30.524],
[-71.225, -30.525],
[-30.525, -71.225],
[30.525, -71.225],
[71.224, -30.525],
[71.224, 30.524]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [295.322, 295.323],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [315.322, 315.323],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [295.322, 295.323] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.426, 0],
[0, 8.426],
[0, 0],
[-8.426, 0],
[0, -8.426],
[0, 0]
],
"o": [
[-8.426, 0],
[0, 0],
[0, -8.426],
[8.426, 0],
[0, 0],
[0, 8.426]
],
"v": [
[0, 30.525],
[-15.262, 15.263],
[-15.262, -15.262],
[0, -30.525],
[15.262, -15.262],
[15.262, 15.263]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [76.561, 183.399],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [56.561, 183.399],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [76.561, 183.399] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 2",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 2,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.426, 0],
[0, 8.426],
[5.395, 13.057],
[9.956, 9.956],
[13.037, 5.406],
[14.1, 0],
[0, 8.426],
[-8.426, 0],
[-16.753, -6.955],
[-12.808, -12.818],
[-6.955, -16.773],
[0, -18.105]
],
"o": [
[-8.426, 0],
[0, -14.09],
[-5.416, -13.016],
[-9.957, -9.956],
[-13.026, -5.385],
[-8.426, 0],
[0, -8.426],
[18.134, 0],
[16.763, 6.936],
[12.788, 12.778],
[6.936, 16.773],
[0, 8.426]
],
"v": [
[61.049, 76.312],
[45.788, 61.05],
[37.66, 20.151],
[14.497, -14.487],
[-20.161, -37.659],
[-61.049, -45.787],
[-76.311, -61.049],
[-61.049, -76.312],
[-8.475, -65.839],
[36.09, -36.069],
[65.858, 8.486],
[76.311, 61.05]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [254.447, 112.349],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [274.447, 92.349],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [254.447, 112.349] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 3",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 3,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.446, 0],
[0, -22.437],
[-22.446, 0],
[0, 22.436]
],
"o": [
[-22.446, 0],
[0, 22.436],
[22.446, 0],
[0, -22.437]
],
"v": [
[0, -40.7],
[-40.7, 0],
[0, 40.699],
[40.7, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[39.269, 0],
[0, 39.268],
[-39.269, 0],
[0, -39.269]
],
"o": [
[-39.269, 0],
[0, -39.269],
[39.269, 0],
[0, 39.268]
],
"v": [
[0, 71.224],
[-71.224, 0],
[0, -71.225],
[71.224, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [71.474, 295.323],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [51.474, 315.323],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [71.474, 295.323] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 4",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 4,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.446, 0],
[0, -22.437],
[-22.446, 0],
[0, 22.436]
],
"o": [
[-22.446, 0],
[0, 22.436],
[22.446, 0],
[0, -22.437]
],
"v": [
[0, -40.7],
[-40.7, 0.001],
[0, 40.7],
[40.7, 0.001]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[39.269, 0],
[0, 39.268],
[-39.269, 0],
[0, -39.269]
],
"o": [
[-39.269, 0],
[0, -39.269],
[39.269, 0],
[0, 39.268]
],
"v": [
[0, 71.225],
[-71.224, 0.001],
[0, -71.225],
[71.224, 0.001]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [71.474, 71.475],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [51.474, 51.475],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [71.474, 71.475] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 5",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 5,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 60.0000024438501,
"st": 0,
"bm": 0
}
],
"markers": []
}
@@ -0,0 +1,521 @@
{
"v": "5.9.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 60.0000024438501,
"w": 500,
"h": 500,
"nm": "page-hover",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Layer 1/paper-edgeless-icons Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [250, 250, 0], "ix": 2, "l": 2 },
"a": { "a": 0, "k": [183, 183, 0], "ix": 1, "l": 2 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.557, 0],
[0, 0],
[0, 8.639],
[-8.64, 0],
[0, 0],
[-2.201, 1.651],
[0, 9.536],
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[16.892, -16.892]
],
"o": [
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[17.442, 0],
[10.412, -10.453],
[0, 0],
[0, -8.64],
[8.639, 0],
[0, 0],
[0, 18.155],
[-9.027, 8.987]
],
"v": [
[46.947, 182.579],
[-109.545, 182.579],
[-125.194, 166.93],
[-109.545, 151.281],
[46.947, 151.281],
[78.001, 145.086],
[93.895, 114.766],
[93.895, -166.93],
[109.545, -182.579],
[125.194, -166.93],
[125.194, 114.766],
[99.743, 167.561]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [240.204, 182.829],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [260.204, 202.829],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [240.204, 182.829] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.64, 0],
[0, 8.639],
[0, 0],
[-16.892, 16.882],
[-22.598, 0],
[0, 0],
[0, -8.64],
[8.639, 0],
[0, 0],
[2.171, -1.65],
[0, -9.546],
[0, 0]
],
"o": [
[-8.64, 0],
[0, 0],
[0, -18.145],
[8.976, -8.986],
[0, 0],
[8.639, 0],
[0, 8.64],
[0, 0],
[-17.453, 0],
[-10.443, 10.484],
[0, 0],
[0, 8.639]
],
"v": [
[-109.545, 182.579],
[-125.194, 166.93],
[-125.194, -114.766],
[-99.743, -167.562],
[-46.948, -182.579],
[109.545, -182.579],
[125.194, -166.93],
[109.545, -151.281],
[-46.948, -151.281],
[-77.972, -145.107],
[-93.896, -114.766],
[-93.896, 166.93]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [125.443, 182.829],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [105.443, 162.829],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [125.443, 182.829] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 2",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 2,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.64, 0],
[0, 0],
[0, 8.639],
[-8.64, 0],
[0, 0],
[0, -8.64]
],
"o": [
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[8.64, 0],
[0, 8.639]
],
"v": [
[67.813, 15.649],
[-67.813, 15.649],
[-83.463, 0],
[-67.813, -15.649],
[67.813, -15.649],
[83.462, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [182.824, 271.513],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [212.824, 281.513],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [182.824, 271.513] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 3",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 3,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, -5.757],
[0, 0],
[-5.756, 0],
[0, 0],
[0, 5.756],
[0, 0],
[5.747, 0]
],
"o": [
[-5.756, 0],
[0, 0],
[0, 5.756],
[0, 0],
[5.747, 0],
[0, 0],
[0, -5.757],
[0, 0]
],
"v": [
[-41.732, -31.294],
[-52.165, -20.86],
[-52.165, 20.87],
[-41.732, 31.303],
[41.73, 31.303],
[52.163, 20.87],
[52.163, -20.86],
[41.73, -31.294]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[23.006, 0],
[0, 0],
[0, 23.015],
[0, 0],
[-23.015, 0],
[0, 0],
[0, -23.016],
[0, 0]
],
"o": [
[0, 0],
[-23.015, 0],
[0, 0],
[0, -23.016],
[0, 0],
[23.006, 0],
[0, 0],
[0, 23.015]
],
"v": [
[41.73, 62.592],
[-41.732, 62.592],
[-83.463, 20.87],
[-83.463, -20.86],
[-41.732, -62.592],
[41.73, -62.592],
[83.463, -20.86],
[83.463, 20.87]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [182.824, 141.088],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [152.824, 131.088],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [182.824, 141.088] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 4",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 4,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 60.0000024438501,
"st": 0,
"bm": 0
}
],
"markers": []
}
@@ -0,0 +1,105 @@
import { Tooltip } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtom } from 'jotai';
import type { CSSProperties } from 'react';
import { useEffect } from 'react';
import { pageSettingFamily } from '../../../../atoms';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
export type EditorModeSwitchProps = {
// todo(himself65): combine these two properties
blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string;
style?: CSSProperties;
};
const TooltipContent = () => {
const t = useAFFiNEI18N();
return (
<div>
{t['Switch']()}
<StyledKeyboardItem>
{!environment.isServer && environment.isMacOs ? '⌥ + S' : 'Alt + S'}
</StyledKeyboardItem>
</div>
);
};
export const EditorModeSwitch = ({
style,
blockSuiteWorkspace,
pageId,
}: EditorModeSwitchProps) => {
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
const currentMode = setting?.mode ?? 'page';
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const t = useAFFiNEI18N();
assertExists(pageMeta);
const { trash } = pageMeta;
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if (
!environment.isServer && environment.isMacOs
? e.key === 'ß'
: e.key === 's' && e.altKey
) {
e.preventDefault();
setSetting(setting => {
if (setting?.mode !== 'page') {
toast(t['com.affine.pageMode']());
return { ...setting, mode: 'page' };
} else {
toast(t['com.affine.edgelessMode']());
return { ...setting, mode: 'edgeless' };
}
});
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [setSetting, t]);
return (
<Tooltip content={<TooltipContent />}>
<StyledEditorModeSwitch
style={style}
switchLeft={currentMode === 'page'}
showAlone={trash}
>
<PageSwitchItem
data-testid="switch-page-mode-button"
active={currentMode === 'page'}
hide={trash && currentMode !== 'page'}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'page') {
toast(t['com.affine.pageMode']());
}
return { ...setting, mode: 'page' };
});
}}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={currentMode === 'edgeless'}
hide={trash && currentMode !== 'edgeless'}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'edgeless') {
toast(t['com.affine.edgelessMode']());
}
return { ...setting, mode: 'edgeless' };
});
}}
/>
</StyledEditorModeSwitch>
</Tooltip>
);
};
@@ -0,0 +1,66 @@
import { displayFlex, styled } from '@affine/component';
export const StyledEditorModeSwitch = styled('div')<{
switchLeft: boolean;
showAlone?: boolean;
}>(({ switchLeft, showAlone }) => {
return {
width: showAlone ? '40px' : '78px',
height: '32px',
background: showAlone
? 'transparent'
: 'var(--affine-background-secondary-color)',
borderRadius: '12px',
...displayFlex('space-between', 'center'),
padding: '0 8px',
position: 'relative',
'::after': {
content: '""',
display: showAlone ? 'none' : 'block',
width: '24px',
height: '24px',
background: 'var(--affine-background-primary-color)',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '8px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '38px'})`,
transition: 'all .15s',
},
};
});
export const StyledSwitchItem = styled('button')<{
active?: boolean;
hide?: boolean;
}>(({ active = false, hide = false }) => {
return {
width: '24px',
height: '24px',
borderRadius: '8px',
WebkitAppRegion: 'no-drag',
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 2,
fontSize: '20px',
path: {
fill: 'currentColor',
},
};
});
export const StyledKeyboardItem = styled('span')(() => {
return {
marginLeft: '10px',
fontSize: 'var(--affine-font-xs)',
paddingLeft: '5px',
paddingRight: '5px',
backgroundColor: 'var(--affine-white-10)',
borderRadius: '4px',
};
});
@@ -0,0 +1,79 @@
import { InternalLottie } from '@affine/component/internal-lottie';
import type { HTMLAttributes } from 'react';
import type React from 'react';
import { cloneElement, useState } from 'react';
import { StyledSwitchItem } from './style';
type HoverAnimateControllerProps = {
active?: boolean;
hide?: boolean;
children: React.ReactElement;
} & HTMLAttributes<HTMLButtonElement>;
const HoverAnimateController = ({
active,
hide,
children,
...props
}: HoverAnimateControllerProps) => {
const [startAnimate, setStartAnimate] = useState(false);
return (
<StyledSwitchItem
hide={hide}
active={active}
onMouseEnter={() => {
setStartAnimate(true);
}}
onMouseLeave={() => {
setStartAnimate(false);
}}
{...props}
>
{cloneElement(children, {
isStopped: !startAnimate,
speed: 5,
width: 20,
height: 20,
})}
</StyledSwitchItem>
);
};
export const PageSwitchItem = (
props: Omit<HoverAnimateControllerProps, 'children'>
) => {
return (
<HoverAnimateController {...props}>
<InternalLottie
options={{
loop: false,
autoplay: false,
animationData: require('./animation-data/page-hover.json'),
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
}}
/>
</HoverAnimateController>
);
};
export const EdgelessSwitchItem = (
props: Omit<HoverAnimateControllerProps, 'children'>
) => {
return (
<HoverAnimateController {...props}>
<InternalLottie
options={{
loop: false,
autoplay: false,
animationData: require('./animation-data/edgeless-hover.json'),
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
}}
/>
</HoverAnimateController>
);
};
@@ -0,0 +1,164 @@
// fixme(himself65): refactor this file
import { FlexWrapper, IconButton, Menu, MenuItem } from '@affine/component';
import { Export, MoveToTrash } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import {
EdgelessIcon,
FavoritedIcon,
FavoriteIcon,
MoreVerticalIcon,
PageIcon,
} from '@blocksuite/icons';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { useAtom, useAtomValue } from 'jotai';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { pageSettingFamily } from '../../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { toast } from '../../../../utils';
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
import * as styles from '../styles.css';
import { LanguageMenu } from './language-menu';
const CommonMenu = () => {
const content = (
<div
onClick={e => {
e.stopPropagation();
}}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
);
return (
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
content={content}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon />
</IconButton>
</Menu>
</FlexWrapper>
);
};
const PageMenu = () => {
const t = useAFFiNEI18N();
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const pageId = useAtomValue(currentPageIdAtom);
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
const mode = setting?.mode ?? 'page';
const favorite = pageMeta.favorite ?? false;
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const EditMenu = (
<>
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite
? t['Removed from Favorites']()
: t['Added to Favorites']()
);
}}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
<div className={styles.horizontalDividerContainer}>
<div className={styles.horizontalDivider} />
</div>
</>
<div
onClick={e => {
e.stopPropagation();
}}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
</>
);
return (
<>
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
content={EditMenu}
placement="bottom-end"
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon />
</IconButton>
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
title={pageMeta.title}
onConfirm={() => {
removeToTrash(pageMeta.id);
toast(t['Moved to Trash']());
setOpenConfirm(false);
}}
onCancel={() => {
setOpenConfirm(false);
}}
/>
</FlexWrapper>
</>
);
};
export const EditorOptionMenu = () => {
const { pageId } = useParams();
return pageId ? <PageMenu /> : <CommonMenu />;
};
@@ -0,0 +1,135 @@
import { Button, displayFlex, Menu, MenuItem, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useI18N } from '@affine/i18n';
import { ArrowDownSmallIcon, LanguageIcon } from '@blocksuite/icons';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
const LanguageMenuContent: FC = () => {
const i18n = useI18N();
const changeLanguage = useCallback(
(event: string) => {
i18n.changeLanguage(event).catch(err => {
console.error(err);
});
},
[i18n]
);
return (
<>
{LOCALES.map(option => {
return (
<StyledListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</StyledListItem>
);
})}
</>
);
};
export const LanguageMenu: React.FC = () => {
const i18n = useI18N();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<StyledContainer>
<StyledIconContainer>
<LanguageIcon />
</StyledIconContainer>
<StyledButtonContainer>
<Menu
content={(<LanguageMenuContent />) as ReactElement}
placement="bottom"
trigger="click"
disablePortal={true}
>
<StyledButton
icon={
<StyledArrowDownContainer>
<ArrowDownSmallIcon />
</StyledArrowDownContainer>
}
iconPosition="end"
noBorder={true}
data-testid="language-menu-button"
>
<StyledCurrentLanguage>
{currentLanguage?.originalName}
</StyledCurrentLanguage>
</StyledButton>
</Menu>
</StyledButtonContainer>
</StyledContainer>
);
};
const StyledListItem = styled(MenuItem)(() => ({
width: '132px',
height: '38px',
fontSize: 'var(--affine-font-base)',
textTransform: 'capitalize',
}));
const StyledContainer = styled('div')(() => {
return {
width: '100%',
height: '48px',
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
padding: '0 14px',
};
});
const StyledIconContainer = styled('div')(() => {
return {
width: '20px',
height: '20px',
color: 'var(--affine-icon-color)',
fontSize: '20px',
...displayFlex('flex-start', 'center'),
};
});
const StyledButtonContainer = styled('div')(() => {
return {
width: '100%',
height: '32px',
borderRadius: '4px',
border: `1px solid var(--affine-border-color)`,
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
marginLeft: '12px',
};
});
const StyledButton = styled(Button)(() => {
return {
width: '100%',
height: '32px',
borderRadius: '4px',
backgroundColor: 'transparent',
...displayFlex('space-between', 'center'),
textTransform: 'capitalize',
padding: '0',
};
});
const StyledArrowDownContainer = styled('div')(() => {
return {
height: '32px',
borderLeft: `1px solid var(--affine-border-color)`,
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
padding: '4px 6px',
fontSize: '24px',
};
});
const StyledCurrentLanguage = styled('div')(() => {
return {
marginLeft: '12px',
color: 'var(--affine-text-color)',
};
});
@@ -0,0 +1,44 @@
import type { CSSProperties, DOMAttributes } from 'react';
type IconProps = {
style?: CSSProperties;
} & DOMAttributes<SVGElement>;
export const MoonIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.54893 3.31407C9.33328 3.08158 9.27962 2.74521 9.41255 2.45912C9.54547 2.17302 9.83936 1.99233 10.1595 1.99986C13.4456 2.07712 16.5114 4.08044 17.7359 7.29071C19.3437 11.5057 17.1672 16.2024 12.8744 17.781C9.60251 18.9843 6.04745 18.0285 3.82974 15.6428C3.61375 15.4104 3.55978 15.0739 3.69257 14.7876C3.82537 14.5014 4.11931 14.3205 4.43962 14.3279C5.27228 14.3474 6.12412 14.2171 6.94979 13.9135C10.415 12.6391 12.172 8.84782 10.8741 5.44537C10.5657 4.63692 10.1061 3.91474 9.54893 3.31407Z"
/>
</svg>
);
};
export const SunIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.8002 2.5002C10.8002 2.05837 10.442 1.7002 10.0002 1.7002C9.55837 1.7002 9.2002 2.05837 9.2002 2.5002V3.33353C9.2002 3.77536 9.55837 4.13353 10.0002 4.13353C10.442 4.13353 10.8002 3.77536 10.8002 3.33353V2.5002ZM5.14921 4.01784C4.83679 3.70542 4.33026 3.70542 4.01784 4.01784C3.70542 4.33026 3.70542 4.83679 4.01784 5.14921L4.69627 5.82764C5.00869 6.14006 5.51522 6.14006 5.82764 5.82764C6.14006 5.51522 6.14006 5.00869 5.82764 4.69627L5.14921 4.01784ZM15.9825 5.1492C16.2949 4.83678 16.2949 4.33025 15.9825 4.01783C15.6701 3.70542 15.1636 3.70543 14.8511 4.01785L14.1727 4.69628C13.8603 5.00871 13.8603 5.51524 14.1728 5.82765C14.4852 6.14007 14.9917 6.14006 15.3041 5.82763L15.9825 5.1492ZM10.0002 5.86686C7.71742 5.86686 5.86686 7.71742 5.86686 10.0002C5.86686 12.283 7.71742 14.1335 10.0002 14.1335C12.283 14.1335 14.1335 12.283 14.1335 10.0002C14.1335 7.71742 12.283 5.86686 10.0002 5.86686ZM2.5002 9.2002C2.05837 9.2002 1.7002 9.55837 1.7002 10.0002C1.7002 10.442 2.05837 10.8002 2.5002 10.8002H3.33353C3.77536 10.8002 4.13353 10.442 4.13353 10.0002C4.13353 9.55837 3.77536 9.2002 3.33353 9.2002H2.5002ZM16.6669 9.2002C16.225 9.2002 15.8669 9.55837 15.8669 10.0002C15.8669 10.442 16.225 10.8002 16.6669 10.8002H17.5002C17.942 10.8002 18.3002 10.442 18.3002 10.0002C18.3002 9.55837 17.942 9.2002 17.5002 9.2002H16.6669ZM5.82623 15.309C6.13943 14.9973 6.14069 14.4908 5.82906 14.1776C5.51742 13.8644 5.01089 13.8631 4.69769 14.1748L4.01926 14.8498C3.70606 15.1615 3.70479 15.668 4.01643 15.9812C4.32807 16.2944 4.8346 16.2956 5.1478 15.984L5.82623 15.309ZM15.3027 14.1748C14.9895 13.8631 14.483 13.8644 14.1713 14.1776C13.8597 14.4908 13.861 14.9973 14.1742 15.3089L14.8526 15.984C15.1658 16.2956 15.6723 16.2944 15.9839 15.9812C16.2956 15.668 16.2943 15.1615 15.9811 14.8498L15.3027 14.1748ZM10.8002 16.6669C10.8002 16.225 10.442 15.8669 10.0002 15.8669C9.55837 15.8669 9.2002 16.225 9.2002 16.6669V17.5002C9.2002 17.942 9.55837 18.3002 10.0002 18.3002C10.442 18.3002 10.8002 17.942 10.8002 17.5002V16.6669Z"
/>
</svg>
);
};
@@ -0,0 +1,61 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DarkModeIcon, LightModeIcon } from '@blocksuite/icons';
import { useTheme } from 'next-themes';
import {
StyledSwitchItem,
StyledThemeButton,
StyledThemeButtonContainer,
StyledThemeModeContainer,
StyledThemeModeSwitch,
StyledVerticalDivider,
} from './style';
export const MenuThemeModeSwitch = () => {
const { setTheme, resolvedTheme, theme } = useTheme();
const t = useAFFiNEI18N();
return (
<StyledThemeModeContainer>
<StyledThemeModeSwitch data-testid="change-theme-container" inMenu={true}>
<StyledSwitchItem active={resolvedTheme === 'light'} inMenu={true}>
<LightModeIcon />
</StyledSwitchItem>
<StyledSwitchItem active={resolvedTheme === 'dark'} inMenu={true}>
<DarkModeIcon />
</StyledSwitchItem>
</StyledThemeModeSwitch>
<StyledThemeButtonContainer>
<StyledThemeButton
data-testid="change-theme-light"
active={theme === 'light'}
onClick={() => {
setTheme('light');
}}
>
{t['light']()}
</StyledThemeButton>
<StyledVerticalDivider />
<StyledThemeButton
data-testid="change-theme-dark"
active={theme === 'dark'}
onClick={() => {
setTheme('dark');
}}
>
{t['dark']()}
</StyledThemeButton>
<StyledVerticalDivider />
<StyledThemeButton
active={theme === 'system'}
onClick={() => {
setTheme('system');
}}
>
{t['system']()}
</StyledThemeButton>
</StyledThemeButtonContainer>
</StyledThemeModeContainer>
);
};
export default MenuThemeModeSwitch;
@@ -0,0 +1,120 @@
import { css, displayFlex, keyframes, styled } from '@affine/component';
// @ts-expect-error: no types for css-spring
import spring, { toString } from 'css-spring';
const ANIMATE_DURATION = 400;
export const StyledThemeModeContainer = styled('div')(() => {
return {
width: '100%',
height: '48px',
borderRadius: '6px',
backgroundColor: 'transparent',
color: 'var(--affine-icon-color)',
fontSize: '16px',
...displayFlex('flex-start', 'center'),
padding: '0 14px',
};
});
export const StyledThemeButtonContainer = styled('div')(() => {
return {
height: '32px',
border: `1px solid var(--affine-border-color)`,
borderRadius: '4px',
cursor: 'pointer',
...displayFlex('space-evenly', 'center'),
flexGrow: 1,
marginLeft: '12px',
};
});
export const StyledThemeButton = styled('button')<{
active: boolean;
}>(({ active }) => {
return {
padding: '0 8px',
height: '100%',
flex: 1,
cursor: 'pointer',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
whiteSpace: 'nowrap',
};
});
export const StyledVerticalDivider = styled('div')(() => {
return {
width: '1px',
height: '100%',
borderLeft: `1px solid var(--affine-border-color)`,
};
});
export const StyledThemeModeSwitch = styled('button')<{
inMenu?: boolean;
}>(({ inMenu }) => {
return {
width: inMenu ? '20px' : '32px',
height: inMenu ? '20px' : '32px',
borderRadius: '6px',
overflow: 'hidden',
WebkitAppRegion: 'no-drag',
backgroundColor: 'transparent',
position: 'relative',
color: 'var(--affine-icon-color)',
fontSize: inMenu ? '20px' : '24px',
};
});
export const StyledSwitchItem = styled('div')<{
active: boolean;
isHover?: boolean;
inMenu?: boolean;
}>(({ active, isHover, inMenu }) => {
const activeRaiseAnimate = toString(
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
);
const raiseAnimate = toString(
spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' })
);
const activeDeclineAnimate = toString(
spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' })
);
const declineAnimate = toString(
spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' })
);
const activeStyle = active
? {
color: 'var(--affine-icon-color)',
top: '0',
animation: css`
${keyframes`${
isHover ? activeRaiseAnimate : activeDeclineAnimate
}`} ${ANIMATE_DURATION}ms forwards
`,
animationDirection: isHover ? 'normal' : 'alternate',
}
: {
top: '100%',
color: 'var(--affine-primary-color)',
backgroundColor: 'var(--affine-hover-color)',
animation: css`
${keyframes`${
isHover ? raiseAnimate : declineAnimate
}`} ${ANIMATE_DURATION}ms forwards
`,
animationDirection: isHover ? 'normal' : 'alternate',
};
return css`
${css(displayFlex('center', 'center'))}
width:${inMenu ? '20px' : '32px'} ;
height: ${inMenu ? '20px' : '32px'} ;
position: absolute;
left: 0;
cursor: pointer;
color: ${activeStyle.color}
top: ${activeStyle.top};
background-color: ${activeStyle.backgroundColor};
animation: ${activeStyle.animation};
animation-direction: ${activeStyle.animationDirection};
//svg {
// width: 24px;
// height: 24px;
//},
`;
});
@@ -0,0 +1,74 @@
import { Button, Confirm } from '@affine/component';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
export const TrashButtonGroup = () => {
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const pageId = useAtomValue(currentPageIdAtom);
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const t = useAFFiNEI18N();
const { jumpToSubPath } = useNavigateHelper();
const { restoreFromTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const [open, setOpen] = useState(false);
return (
<>
<Button
bold={true}
shape="round"
style={{ marginRight: '24px' }}
onClick={() => {
restoreFromTrash(pageId);
}}
>
{t['Restore it']()}
</Button>
<Button
bold={true}
shape="round"
type="danger"
onClick={() => {
setOpen(true);
}}
>
{t['Delete permanently']()}
</Button>
<Confirm
title={t['TrashButtonGroupTitle']()}
content={t['TrashButtonGroupDescription']()}
confirmText={t['Delete']()}
confirmType="danger"
open={open}
onConfirm={useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
blockSuiteWorkspace.removePage(pageId);
}, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id])}
onCancel={() => {
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
/>
</>
);
};
export default TrashButtonGroup;
@@ -0,0 +1,101 @@
import { Menu, MenuItem } from '@affine/component';
import { AffineLogoSBlue2_1Icon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
const EditMenu = (
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
Sign Out
</MenuItem>
);
export const UserAvatar = () => {
// fixme: cloud regression
const user: any = null;
return (
<Menu
width={276}
content={EditMenu}
placement="bottom"
disablePortal={true}
trigger="click"
>
{user ? (
<WorkspaceAvatar
size={24}
name={user.name}
avatar={user.avatar_url}
></WorkspaceAvatar>
) : (
<WorkspaceAvatar size={24}></WorkspaceAvatar>
)}
</Menu>
);
};
interface WorkspaceAvatarProps {
size: number;
name?: string;
avatar?: string;
style?: CSSProperties;
}
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
function WorkspaceAvatar(props, ref) {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
{props.name ? (
props.name.substring(0, 1)
) : (
<AffineLogoSBlue2_1Icon fontSize={24} color={'#5438FF'} />
)}
</div>
)}
</>
);
}
);
export default UserAvatar;
@@ -0,0 +1,236 @@
import { BrowserWarning } from '@affine/component/affine-banner';
import {
appSidebarFloatingAtom,
appSidebarOpenAtom,
} from '@affine/component/app-sidebar';
import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
import { isDesktop } from '@affine/env/constant';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import {
forwardRef,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { contentLayoutAtom } from '../../../atoms/layout';
import { currentModeAtom } from '../../../atoms/mode';
import type { AffineOfficialWorkspace } from '../../../shared';
import { EditorOptionMenu } from './header-right-items/editor-option-menu';
import TrashButtonGroup from './header-right-items/trash-button-group';
import * as styles from './styles.css';
import { OSWarningMessage, shouldShowWarning } from './utils';
export type BaseHeaderProps<
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace,
> = {
workspace: Workspace;
currentPage: Page | null;
isPublic: boolean;
leftSlot?: ReactNode;
};
export enum HeaderRightItemName {
EditorOptionMenu = 'editorOptionMenu',
TrashButtonGroup = 'trashButtonGroup',
// some windows only items
WindowsAppControls = 'windowsAppControls',
}
type HeaderItem = {
Component: FC<BaseHeaderProps>;
// todo: public workspace should be one of the flavour
availableWhen: (
workspace: AffineOfficialWorkspace,
currentPage: Page | null,
status: {
isPublic: boolean;
}
) => boolean;
};
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.TrashButtonGroup]: {
Component: TrashButtonGroup,
availableWhen: (_, currentPage) => {
return currentPage?.meta.trash === true;
},
},
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic }) => {
return !isPublic;
},
},
[HeaderRightItemName.WindowsAppControls]: {
Component: () => {
const handleMinimizeApp = useCallback(() => {
window.apis?.ui.handleMinimizeApp().catch(err => {
console.error(err);
});
}, []);
const handleMaximizeApp = useCallback(() => {
window.apis?.ui.handleMaximizeApp().catch(err => {
console.error(err);
});
}, []);
const handleCloseApp = useCallback(() => {
window.apis?.ui.handleCloseApp().catch(err => {
console.error(err);
});
}, []);
return (
<div
data-platform-target="win32"
className={styles.windowAppControlsWrapper}
>
<button
data-type="minimize"
className={styles.windowAppControl}
onClick={handleMinimizeApp}
>
<MinusIcon />
</button>
<button
data-type="maximize"
className={styles.windowAppControl}
onClick={handleMaximizeApp}
>
<RoundedRectangleIcon />
</button>
<button
data-type="close"
className={styles.windowAppControl}
onClick={handleCloseApp}
>
<CloseIcon />
</button>
</div>
);
},
availableWhen: () => {
return isDesktop && globalThis.platform === 'win32';
},
},
};
export type HeaderProps = BaseHeaderProps;
const PluginHeaderItemAdapter = memo<{
headerItem: PluginUIAdapter['headerItem'];
}>(function PluginHeaderItemAdapter({ headerItem }) {
return (
<div>
{headerItem({
contentLayoutAtom,
})}
</div>
);
});
const PluginHeader = () => {
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
return (
<div>
{plugins
.filter(plugin => plugin.uiAdapter.headerItem != null)
.map(plugin => {
const headerItem = plugin.uiAdapter
.headerItem as PluginUIAdapter['headerItem'];
return (
<PluginHeaderItemAdapter
key={plugin.definition.id}
headerItem={headerItem}
/>
);
})}
</div>
);
};
export const Header = forwardRef<
HTMLDivElement,
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const [showWarning, setShowWarning] = useState(false);
// const [shouldShowGuideDownloadClientTip] = useAtom(
// guideDownloadClientTipAtom
// );
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
const open = useAtomValue(appSidebarOpenAtom);
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
const mode = useAtomValue(currentModeAtom);
return (
<div
className={styles.headerContainer}
ref={ref}
data-has-warning={showWarning}
data-open={open}
data-sidebar-floating={appSidebarFloating}
>
<BrowserWarning
show={showWarning}
message={<OSWarningMessage />}
onClose={() => {
setShowWarning(false);
}}
/>
<div
className={styles.header}
data-has-warning={showWarning}
data-testid="editor-header-items"
data-is-edgeless={mode === 'edgeless'}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
{!open && <SidebarSwitch />}
{props.leftSlot}
</div>
{props.children}
<div className={styles.headerRightSide}>
<PluginHeader />
{useMemo(() => {
return Object.entries(HeaderRightItems).map(
([name, { availableWhen, Component }]) => {
if (
availableWhen(props.workspace, props.currentPage, {
isPublic: props.isPublic,
})
) {
return (
<Component
workspace={props.workspace}
currentPage={props.currentPage}
isPublic={props.isPublic}
key={name}
/>
);
}
return null;
}
);
}, [props])}
</div>
</div>
</div>
);
});
Header.displayName = 'Header';
@@ -0,0 +1,65 @@
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useSetAtom } from 'jotai';
import type {
FC,
HTMLAttributes,
PropsWithChildren,
ReactElement,
} from 'react';
import { useRef } from 'react';
import { openQuickSearchModalAtom } from '../../../atoms';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
import type { BaseHeaderProps } from './header';
import { Header } from './header';
import * as styles from './styles.css';
export type WorkspaceHeaderProps = BaseHeaderProps;
export const BlockSuiteEditorHeader: FC<
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
> = (props): ReactElement => {
const { workspace, currentPage, children, isPublic } = props;
// fixme(himself65): remove this atom and move it to props
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
meta => meta.id === currentPage?.id
);
const headerRef = useRef<HTMLDivElement>(null);
assertExists(pageMeta);
const title = pageMeta.title;
return (
<Header ref={headerRef} {...props}>
{children}
{!isPublic && currentPage && (
<div className={styles.titleContainer}>
<div className={styles.titleWrapper}>
<div className={styles.switchWrapper}>
<EditorModeSwitch
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
pageId={currentPage.id}
style={{
marginRight: '12px',
}}
/>
</div>
<div className={styles.title}>{title || 'Untitled'}</div>
<div className={styles.searchArrowWrapper}>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</div>
</div>
</div>
)}
</Header>
);
};
BlockSuiteEditorHeader.displayName = 'BlockSuiteEditorHeader';
@@ -0,0 +1,240 @@
import type { ComplexStyleRule } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const headerContainer = style({
height: '52px',
flexShrink: 0,
position: 'sticky',
top: 0,
background: 'var(--affine-background-primary-color)',
zIndex: 'var(--affine-z-index-popover)',
selectors: {
'&[data-has-warning="true"]': {
height: '96px',
},
'&[data-sidebar-floating="false"]': {
WebkitAppRegion: 'drag',
},
},
'@media': {
print: {
display: 'none',
},
},
':has([data-popper-placement])': {
WebkitAppRegion: 'no-drag',
},
} as ComplexStyleRule);
export const header = style({
flexShrink: 0,
height: '52px',
width: '100%',
padding: '0 20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: 'var(--affine-background-primary-color)',
zIndex: 99,
position: 'relative',
selectors: {
'&[data-is-edgeless="true"]': {
borderBottom: `1px solid var(--affine-border-color)`,
},
},
});
export const titleContainer = style({
width: '100%',
height: '100%',
margin: 'auto',
position: 'absolute',
inset: 'auto auto auto 50%',
transform: 'translate(-50%, 0px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignContent: 'unset',
fontSize: 'var(--affine-font-base)',
});
export const title = style({
maxWidth: '620px',
transition: 'max-width .15s',
userSelect: 'none',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'@media': {
'(max-width: 768px)': {
selectors: {
'&[data-open="true"]': {
WebkitAppRegion: 'no-drag',
},
},
},
},
} as ComplexStyleRule);
export const titleWrapper = style({
height: '100%',
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const headerRightSide = style({
height: '100%',
display: 'flex',
alignItems: 'center',
gap: '12px',
zIndex: 1,
});
export const browserWarning = style({
backgroundColor: 'var(--affine-background-warning-color)',
color: 'var(--affine-warning-color)',
height: '36px',
fontSize: 'var(--affine-font-sm)',
display: 'none',
justifyContent: 'center',
alignItems: 'center',
selectors: {
'&[data-show="true"]': {
display: 'flex',
},
},
});
export const closeButton = style({
width: '36px',
height: '36px',
color: 'var(--affine-icon-color)',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
right: '15px',
top: 0,
});
export const switchWrapper = style({
position: 'absolute',
right: '100%',
top: 0,
bottom: 0,
margin: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const searchArrowWrapper = style({
position: 'absolute',
left: 'calc(100% + 4px)',
top: 0,
bottom: 0,
margin: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const pageListTitleWrapper = style({
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const allPageListTitleWrapper = style({
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'::after': {
content: '""',
display: 'block',
width: '100%',
height: '1px',
background: 'var(--affine-border-color)',
position: 'absolute',
bottom: 0,
left: 0,
margin: '0 1px',
},
});
export const pageListTitleIcon = style({
fontSize: '20px',
height: '1em',
marginRight: '12px',
});
export const quickSearchTipButton = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '12px',
color: '#FFFFFF',
width: '48px',
height: ' 26px',
fontSize: 'var(--affine-font-sm)',
lineHeight: '22px',
background: 'var(--affine-primary-color)',
borderRadius: '8px',
textAlign: 'center',
cursor: 'pointer',
});
export const quickSearchTipContent = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
flexDirection: 'column',
});
export const horizontalDivider = style({
width: '100%',
borderTop: `1px solid var(--affine-border-color)`,
});
export const horizontalDividerContainer = style({
width: '100%',
padding: '14px',
});
export const windowAppControlsWrapper = style({
display: 'flex',
gap: '2px',
transform: 'translateX(8px)',
height: '100%',
});
export const windowAppControl = style({
WebkitAppRegion: 'no-drag',
cursor: 'pointer',
display: 'inline-flex',
width: '42px',
height: 'calc(100% - 10px)',
paddingTop: '10px',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '0',
selectors: {
'&[data-type="close"]': {
width: '56px',
paddingRight: '14px',
marginRight: '-14px',
},
'&[data-type="close"]:hover': {
background: 'var(--affine-error-color)',
color: '#FFFFFF',
},
'&:hover': {
background: 'var(--affine-background-tertiary-color)',
},
},
} as ComplexStyleRule);
@@ -0,0 +1,52 @@
import { isDesktop } from '@affine/env/constant';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type React from 'react';
import { useEffect, useState } from 'react';
const minimumChromeVersion = 102;
export const shouldShowWarning = () => {
if (isDesktop) {
// even though desktop has compatibility issues,
// we don't want to show the warning
return false;
}
if (!environment.isBrowser) {
// disable in SSR
return false;
}
if (environment.isChrome) {
return environment.chromeVersion < minimumChromeVersion;
} else {
return !environment.isMobile;
}
};
export const OSWarningMessage: React.FC = () => {
const t = useAFFiNEI18N();
const [notChrome, setNotChrome] = useState(false);
const [notGoodVersion, setNotGoodVersion] = useState(false);
useEffect(() => {
setNotChrome(environment.isBrowser && !environment.isChrome);
setNotGoodVersion(
environment.isBrowser &&
environment.isChrome &&
environment.chromeVersion < minimumChromeVersion
);
}, []);
if (notChrome) {
return (
<span>
<Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for optimal
experience.
</Trans>
</span>
);
} else if (notGoodVersion) {
return <span>{t['upgradeBrowser']()}</span>;
}
return null;
};