mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
milestone: publish alpha version (#637)
- document folder - full-text search - blob storage - basic edgeless support Co-authored-by: tzhangchi <terry.zhangchi@outlook.com> Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com> Co-authored-by: DiamondThree <diamond.shx@gmail.com> Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Yifeng Wang <doodlewind@toeverything.info> Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
77
packages/app/src/components/header/editor-header.tsx
Normal file
77
packages/app/src/components/header/editor-header.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
StyledSearchArrowWrapper,
|
||||
StyledSwitchWrapper,
|
||||
StyledTitle,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
import { Content } from '@/ui/layout';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import EditorModeSwitch from '@/components/editor-mode-switch';
|
||||
import QuickSearchButton from './quick-search-button';
|
||||
import Header from './header';
|
||||
import usePropsUpdated from '@/hooks/use-props-updated';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
|
||||
export const EditorHeader = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const { editor } = useAppState();
|
||||
const { trash: isTrash = false } = useCurrentPageMeta() || {};
|
||||
const onPropsUpdated = usePropsUpdated();
|
||||
|
||||
useEffect(() => {
|
||||
onPropsUpdated(editor => {
|
||||
setTitle(editor.model?.title || 'Untitled');
|
||||
});
|
||||
}, [onPropsUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
// If first time in, need to wait for editor to be inserted into DOM
|
||||
setTitle(editor?.model?.title || 'Untitled');
|
||||
}, 300);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
rightItems={
|
||||
isTrash
|
||||
? ['trashButtonGroup']
|
||||
: ['syncUser', 'themeModeSwitch', 'editorOptionMenu']
|
||||
}
|
||||
>
|
||||
{title && (
|
||||
<StyledTitle
|
||||
onMouseEnter={() => {
|
||||
if (isTrash) return;
|
||||
|
||||
setIsHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isTrash) return;
|
||||
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<StyledTitleWrapper>
|
||||
<StyledSwitchWrapper>
|
||||
<EditorModeSwitch
|
||||
isHover={isHover}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
</StyledSwitchWrapper>
|
||||
<Content ellipsis={true}>{title}</Content>
|
||||
<StyledSearchArrowWrapper>
|
||||
<QuickSearchButton />
|
||||
</StyledSearchArrowWrapper>
|
||||
</StyledTitleWrapper>
|
||||
</StyledTitle>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorHeader;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
ExportIcon,
|
||||
ExportToHtmlIcon,
|
||||
ExportToMarkdownIcon,
|
||||
FavouritedIcon,
|
||||
FavouritesIcon,
|
||||
MoreVerticalIcon,
|
||||
PaperIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { toast } from '@/ui/toast';
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor } = useAppState();
|
||||
const { toggleFavoritePage, toggleDeletePage } = usePageHelper();
|
||||
const { changePageMode } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
const {
|
||||
mode = 'page',
|
||||
id = '',
|
||||
favorite = false,
|
||||
title = '',
|
||||
} = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={() => {
|
||||
toggleFavoritePage(id);
|
||||
toast(!favorite ? 'Removed to Favourites' : 'Added to Favourites');
|
||||
}}
|
||||
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
>
|
||||
{favorite ? 'Remove' : 'Add'} to favourites
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={() => {
|
||||
changePageMode(id, mode === 'page' ? 'edgeless' : 'page');
|
||||
}}
|
||||
>
|
||||
Convert to {mode === 'page' ? 'Edgeless' : 'Page'}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
placement="left-start"
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportHtml();
|
||||
}}
|
||||
icon={<ExportToHtmlIcon />}
|
||||
>
|
||||
Export to HTML
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportMarkdown();
|
||||
}}
|
||||
icon={<ExportToMarkdownIcon />}
|
||||
>
|
||||
Export to Markdown
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MenuItem icon={<ExportIcon />} isDir={true}>
|
||||
Export
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-delete"
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete page?',
|
||||
content: `${title || 'Untitled'} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && toggleDeletePage(id);
|
||||
toast('Moved to Trash');
|
||||
});
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorOptionMenu = () => {
|
||||
return (
|
||||
<Menu content={<PopoverContent />} placement="bottom-end">
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorOptionMenu;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const SyncUser = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
const appState = useAppState();
|
||||
|
||||
return appState.user ? (
|
||||
<IconButton iconSize="middle" disabled>
|
||||
<CloudInsyncIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
iconSize="middle"
|
||||
data-testid="cloud-unsync-icon"
|
||||
onClick={triggerLoginModal}
|
||||
>
|
||||
<CloudUnsyncedIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncUser;
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { DOMAttributes, CSSProperties } 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,49 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
import { MoonIcon, SunIcon } from './icons';
|
||||
import { StyledThemeModeSwitch, StyledSwitchItem } from './style';
|
||||
|
||||
export const ThemeModeSwitch = () => {
|
||||
const { mode, changeMode } = useTheme();
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const [firstTrigger, setFirstTrigger] = useState(false);
|
||||
return (
|
||||
<StyledThemeModeSwitch
|
||||
data-testid="change-theme-container"
|
||||
onMouseEnter={() => {
|
||||
setIsHover(true);
|
||||
if (!firstTrigger) {
|
||||
setFirstTrigger(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<StyledSwitchItem
|
||||
data-testid="change-theme-light"
|
||||
active={mode === 'light'}
|
||||
isHover={isHover}
|
||||
firstTrigger={firstTrigger}
|
||||
onClick={() => {
|
||||
changeMode('light');
|
||||
}}
|
||||
>
|
||||
<SunIcon />
|
||||
</StyledSwitchItem>
|
||||
<StyledSwitchItem
|
||||
data-testid="change-theme-dark"
|
||||
active={mode === 'dark'}
|
||||
isHover={isHover}
|
||||
firstTrigger={firstTrigger}
|
||||
onClick={() => {
|
||||
changeMode('dark');
|
||||
}}
|
||||
>
|
||||
<MoonIcon />
|
||||
</StyledSwitchItem>
|
||||
</StyledThemeModeSwitch>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModeSwitch;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { displayFlex, keyframes, styled } from '@/styles';
|
||||
import { CSSProperties } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import spring, { toString } from 'css-spring';
|
||||
|
||||
const ANIMATE_DURATION = 400;
|
||||
|
||||
export const StyledThemeModeSwitch = styled('div')({
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
});
|
||||
export const StyledSwitchItem = styled('div')<{
|
||||
active: boolean;
|
||||
isHover: boolean;
|
||||
firstTrigger: boolean;
|
||||
}>(({ active, isHover, firstTrigger, theme }) => {
|
||||
const activeRaiseAnimate = keyframes`${toString(
|
||||
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const raiseAnimate = keyframes`${toString(
|
||||
spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const activeDeclineAnimate = keyframes`${toString(
|
||||
spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const declineAnimate = keyframes`${toString(
|
||||
spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const activeStyle = active
|
||||
? {
|
||||
color: theme.colors.iconColor,
|
||||
top: '0',
|
||||
animation: firstTrigger
|
||||
? `${
|
||||
isHover ? activeRaiseAnimate : activeDeclineAnimate
|
||||
} ${ANIMATE_DURATION}ms forwards`
|
||||
: 'unset',
|
||||
animationDirection: isHover ? 'normal' : 'alternate',
|
||||
}
|
||||
: ({
|
||||
top: '100%',
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
animation: firstTrigger
|
||||
? `${
|
||||
isHover ? raiseAnimate : declineAnimate
|
||||
} ${ANIMATE_DURATION}ms forwards`
|
||||
: 'unset',
|
||||
animationDirection: isHover ? 'normal' : 'alternate',
|
||||
} as CSSProperties);
|
||||
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
...displayFlex('center', 'center'),
|
||||
cursor: 'pointer',
|
||||
...activeStyle,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button } from '@/ui/button';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
|
||||
export const TrashButtonGroup = () => {
|
||||
const { permanentlyDeletePage } = usePageHelper();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { toggleDeletePage } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
const router = useRouter();
|
||||
const { id = '' } = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
style={{ marginRight: '24px' }}
|
||||
onClick={() => {
|
||||
toggleDeletePage(id);
|
||||
}}
|
||||
>
|
||||
Restore it
|
||||
</Button>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Permanently delete',
|
||||
content:
|
||||
"Once deleted, you can't undo this action. Do you confirm?",
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
if (confirm) {
|
||||
router.push(`/workspace/${currentWorkspaceId}/all`);
|
||||
permanentlyDeletePage(id);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete permanently
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrashButtonGroup;
|
||||
71
packages/app/src/components/header/header.tsx
Normal file
71
packages/app/src/components/header/header.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { PropsWithChildren, ReactNode, useState } from 'react';
|
||||
import {
|
||||
StyledHeader,
|
||||
StyledHeaderRightSide,
|
||||
StyledHeaderContainer,
|
||||
StyledBrowserWarning,
|
||||
StyledCloseButton,
|
||||
} from './styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import EditorOptionMenu from './header-right-items/editor-option-menu';
|
||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||
// import SyncUser from './header-right-items/sync-user';
|
||||
|
||||
const BrowserWarning = ({
|
||||
show,
|
||||
onClose,
|
||||
}: {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<StyledBrowserWarning show={show}>
|
||||
{getWarningMessage()}
|
||||
<StyledCloseButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</StyledCloseButton>
|
||||
</StyledBrowserWarning>
|
||||
);
|
||||
};
|
||||
|
||||
type HeaderRightItemNames =
|
||||
| 'editorOptionMenu'
|
||||
| 'trashButtonGroup'
|
||||
| 'themeModeSwitch'
|
||||
| 'syncUser';
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
|
||||
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
||||
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
||||
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
||||
syncUser: null,
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
rightItems = ['syncUser'],
|
||||
children,
|
||||
}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => {
|
||||
const [showWarning, setShowWarning] = useState(shouldShowWarning());
|
||||
return (
|
||||
<StyledHeaderContainer hasWarning={showWarning}>
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
<StyledHeader hasWarning={showWarning} data-testid="editor-header-items">
|
||||
{children}
|
||||
<StyledHeaderRightSide>
|
||||
{rightItems.map(itemName => {
|
||||
return HeaderRightItems[itemName];
|
||||
})}
|
||||
</StyledHeaderRightSide>
|
||||
</StyledHeader>
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
3
packages/app/src/components/header/index.tsx
Normal file
3
packages/app/src/components/header/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './header';
|
||||
export * from './editor-header';
|
||||
export * from './page-list-header';
|
||||
21
packages/app/src/components/header/page-list-header.tsx
Normal file
21
packages/app/src/components/header/page-list-header.tsx
Normal file
@@ -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;
|
||||
29
packages/app/src/components/header/quick-search-button.tsx
Normal file
29
packages/app/src/components/header/quick-search-button.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconButton, IconButtonProps } from '@/ui/button';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
export const QuickSearchButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
|
||||
return (
|
||||
<Tooltip content="Switch to" placement="bottom">
|
||||
<IconButton
|
||||
data-testid="header-quickSearchButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearchButton;
|
||||
123
packages/app/src/components/header/styles.ts
Normal file
123
packages/app/src/components/header/styles.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
|
||||
export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
height: hasWarning ? '96px' : '60px',
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledHeader = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning }) => {
|
||||
return {
|
||||
height: '60px',
|
||||
width: '100%',
|
||||
...displayFlex('flex-end', 'center'),
|
||||
background: 'var(--affine-page-background)',
|
||||
transition: 'background-color 0.5s',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: hasWarning ? '36px' : '0',
|
||||
padding: '0 22px',
|
||||
zIndex: 99,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledTitle = styled('div')(({ theme }) => ({
|
||||
width: '720px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
margin: 'auto',
|
||||
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: theme.font.base,
|
||||
}));
|
||||
|
||||
export const StyledTitleWrapper = styled('div')({
|
||||
maxWidth: '720px',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
...displayFlex('center', 'center'),
|
||||
});
|
||||
|
||||
export const StyledHeaderRightSide = styled('div')({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const StyledBrowserWarning = styled.div<{ show: boolean }>(
|
||||
({ theme, show }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.warningBackground,
|
||||
color: theme.colors.warningColor,
|
||||
height: '36px',
|
||||
width: '100vw',
|
||||
fontSize: theme.font.sm,
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
top: '0',
|
||||
display: show ? 'flex' : 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledCloseButton = styled.div(({ theme }) => {
|
||||
return {
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
color: theme.colors.iconColor,
|
||||
cursor: 'pointer',
|
||||
...displayFlex('center', 'center'),
|
||||
position: 'absolute',
|
||||
right: '15px',
|
||||
top: '0',
|
||||
|
||||
svg: {
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSwitchWrapper = styled.div(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSearchArrowWrapper = styled.div(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.sm,
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
38
packages/app/src/components/header/utils.tsx
Normal file
38
packages/app/src/components/header/utils.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import getIsMobile from '@/utils/get-is-mobile';
|
||||
// Inspire by https://stackoverflow.com/a/4900484/8415727
|
||||
const getChromeVersion = () => {
|
||||
const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
return raw ? parseInt(raw[2], 10) : false;
|
||||
};
|
||||
const getIsChrome = () => {
|
||||
return (
|
||||
/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)
|
||||
);
|
||||
};
|
||||
const minimumChromeVersion = 102;
|
||||
|
||||
export const shouldShowWarning = () => {
|
||||
return (
|
||||
!window.CLIENT_APP &&
|
||||
!getIsMobile() &&
|
||||
(!getIsChrome() || getChromeVersion() < minimumChromeVersion)
|
||||
);
|
||||
};
|
||||
|
||||
export const getWarningMessage = () => {
|
||||
if (!getIsChrome()) {
|
||||
return (
|
||||
<span>
|
||||
We recommend the <strong>Chrome</strong> browser for optimal experience.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (getChromeVersion() < minimumChromeVersion) {
|
||||
return (
|
||||
<span>
|
||||
Please upgrade to the latest version of Chrome for the best experience.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
Reference in New Issue
Block a user