From 401fb48b86f2cc02f32ed7731b8348a67675da3b Mon Sep 17 00:00:00 2001 From: Qi <474021214@qq.com> Date: Sat, 12 Aug 2023 04:27:24 +0800 Subject: [PATCH] feat: refator header (#3685) Co-authored-by: JimmFly Co-authored-by: Alex Yang --- apps/core/package.json | 2 + .../block-suite-header-title/index.tsx | 148 ++++++++ .../operation-menu.tsx} | 69 +--- .../block-suite-header-title/styles.css.ts | 31 ++ .../animation-data/edgeless-hover.json | 0 .../animation-data/page-hover.json | 0 .../index.tsx | 6 +- .../style.ts | 0 .../switch-items.tsx | 0 .../workspace-header/download-tips.tsx | 27 -- .../header-right-items/language-menu.tsx | 143 -------- .../theme-mode-switch/icons.tsx | 44 --- .../theme-mode-switch/index.tsx | 61 ---- .../theme-mode-switch/style.ts | 120 ------- .../header-right-items/user-avatar.tsx | 101 ------ .../blocksuite/workspace-header/header.tsx | 258 -------------- .../blocksuite/workspace-header/index.tsx | 100 ------ .../blocksuite/workspace-header/styles.css.ts | 315 ------------------ .../src/components/page-detail-editor.tsx | 2 +- .../core/src/components/pure/header/index.tsx | 150 +++++++++ .../src/components/pure/header/style.css.tsx | 111 ++++++ .../utils.tsx => pure/header/top-tip.tsx} | 40 ++- .../pure/header/windows-app-controls.tsx | 51 +++ .../components/pure/plugin-header/index.tsx | 44 +++ .../pure/plugin-header/styles.css.ts | 8 + .../trash-button-group/index.tsx} | 9 +- .../trash-button-group}/styles.css.ts | 0 .../pure/workspace-mode-filter-tab/index.tsx | 30 ++ .../components/pure/workspace-title/index.tsx | 44 --- apps/core/src/components/workspace-header.tsx | 186 ++++++----- apps/electron/e2e/workspace.spec.ts | 12 +- .../page-list/view/collection-list.tsx | 24 +- tests/kit/utils/filter.ts | 2 +- tests/kit/utils/page-logic.ts | 2 +- yarn.lock | 18 + 35 files changed, 772 insertions(+), 1386 deletions(-) create mode 100644 apps/core/src/components/blocksuite/block-suite-header-title/index.tsx rename apps/core/src/components/blocksuite/{workspace-header/header-right-items/editor-option-menu.tsx => block-suite-header-title/operation-menu.tsx} (75%) create mode 100644 apps/core/src/components/blocksuite/block-suite-header-title/styles.css.ts rename apps/core/src/components/blocksuite/{workspace-header/editor-mode-switch => block-suite-mode-switch}/animation-data/edgeless-hover.json (100%) rename apps/core/src/components/blocksuite/{workspace-header/editor-mode-switch => block-suite-mode-switch}/animation-data/page-hover.json (100%) rename apps/core/src/components/blocksuite/{workspace-header/editor-mode-switch => block-suite-mode-switch}/index.tsx (95%) rename apps/core/src/components/blocksuite/{workspace-header/editor-mode-switch => block-suite-mode-switch}/style.ts (100%) rename apps/core/src/components/blocksuite/{workspace-header/editor-mode-switch => block-suite-mode-switch}/switch-items.tsx (100%) delete mode 100644 apps/core/src/components/blocksuite/workspace-header/download-tips.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header-right-items/language-menu.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/icons.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/index.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/style.ts delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header-right-items/user-avatar.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/header.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/index.tsx delete mode 100644 apps/core/src/components/blocksuite/workspace-header/styles.css.ts create mode 100644 apps/core/src/components/pure/header/index.tsx create mode 100644 apps/core/src/components/pure/header/style.css.tsx rename apps/core/src/components/{blocksuite/workspace-header/utils.tsx => pure/header/top-tip.tsx} (57%) create mode 100644 apps/core/src/components/pure/header/windows-app-controls.tsx create mode 100644 apps/core/src/components/pure/plugin-header/index.tsx create mode 100644 apps/core/src/components/pure/plugin-header/styles.css.ts rename apps/core/src/components/{blocksuite/workspace-header/header-right-items/trash-button-group.tsx => pure/trash-button-group/index.tsx} (88%) rename apps/core/src/components/{blocksuite/workspace-header/header-right-items => pure/trash-button-group}/styles.css.ts (100%) create mode 100644 apps/core/src/components/pure/workspace-mode-filter-tab/index.tsx delete mode 100644 apps/core/src/components/pure/workspace-title/index.tsx diff --git a/apps/core/package.json b/apps/core/package.json index 4c10ff112..e31f69e59 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -34,6 +34,7 @@ "@mui/material": "^5.14.4", "@react-hookz/web": "^23.1.0", "@toeverything/components": "^0.0.10", + "@types/lodash.throttle": "^4.1.7", "async-call-rpc": "^6.3.1", "cmdk": "^0.2.0", "css-spring": "^4.1.0", @@ -43,6 +44,7 @@ "jotai": "^2.3.1", "jotai-devtools": "^0.6.1", "lit": "^2.8.0", + "lodash.throttle": "^4.1.1", "lottie-web": "^5.12.2", "mini-css-extract-plugin": "^2.7.6", "next-themes": "^0.2.1", diff --git a/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx b/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx new file mode 100644 index 000000000..e8523413b --- /dev/null +++ b/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx @@ -0,0 +1,148 @@ +import { WorkspaceFlavour } from '@affine/env/workspace'; +import { + useBlockSuitePageMeta, + usePageMetaHelper, +} from '@toeverything/hooks/use-block-suite-page-meta'; +import { + type FocusEvent, + type InputHTMLAttributes, + type KeyboardEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import type { AffineOfficialWorkspace } from '../../../shared'; +import { EditorModeSwitch } from '../block-suite-mode-switch'; +import { PageMenu } from './operation-menu'; +import * as styles from './styles.css'; + +export interface BlockSuiteHeaderTitleProps { + workspace: AffineOfficialWorkspace; + pageId: string; +} + +const EditableTitle = ({ + value, + onFocus: propsOnFocus, + ...inputProps +}: InputHTMLAttributes) => { + const onFocus = useCallback( + (e: FocusEvent) => { + e.target.select(); + propsOnFocus?.(e); + }, + [propsOnFocus] + ); + return ( +
+ + {value} +
+ ); +}; + +const StableTitle = ({ + workspace, + pageId, + onRename, +}: BlockSuiteHeaderTitleProps & { + onRename?: () => void; +}) => { + const currentPage = workspace.blockSuiteWorkspace.getPage(pageId); + const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find( + meta => meta.id === currentPage?.id + ); + + const title = pageMeta?.title; + + return ( +
+ + + {title || 'Untitled'} + + +
+ ); +}; + +const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => { + const { workspace, pageId } = props; + const currentPage = workspace.blockSuiteWorkspace.getPage(pageId); + const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find( + meta => meta.id === currentPage?.id + ); + const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace); + + const [isEditable, setIsEditable] = useState(false); + const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled'); + + const onRename = useCallback(() => { + setIsEditable(true); + }, []); + + const onBlur = useCallback(() => { + setIsEditable(false); + if (!currentPage?.id) { + return; + } + pageTitleMeta.setPageTitle(currentPage.id, title); + }, [currentPage?.id, pageTitleMeta, title]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Escape') { + onBlur(); + } + }, + [onBlur] + ); + + useEffect(() => { + setPageTitle(pageMeta?.title || ''); + }, [pageMeta?.title]); + + if (isEditable) { + return ( + { + const value = e.target.value; + setPageTitle(value); + }} + /> + ); + } + + return ; +}; + +export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => { + if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) { + return ; + } + return ; +}; + +BlockSuiteHeaderTitle.displayName = 'BlockSuiteHeaderTitle'; diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/editor-option-menu.tsx b/apps/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx similarity index 75% rename from apps/core/src/components/blocksuite/workspace-header/header-right-items/editor-option-menu.tsx rename to apps/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx index 69f03a713..8c82b9194 100644 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/editor-option-menu.tsx +++ b/apps/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx @@ -1,4 +1,3 @@ -// fixme(himself65): refactor this file import { FlexWrapper, Menu, MenuItem } from '@affine/component'; import { Export, MoveToTrash } from '@affine/component/page-list'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; @@ -10,75 +9,41 @@ import { FavoritedIcon, FavoriteIcon, ImportIcon, - MoreVerticalIcon, PageIcon, } from '@blocksuite/icons'; -import { IconButton } from '@toeverything/components/button'; +import type { PageMeta } from '@blocksuite/store'; import { Divider } from '@toeverything/components/divider'; import { useBlockSuitePageMeta, usePageMetaHelper, } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; -import { currentPageIdAtom } from '@toeverything/infra/atom'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useSetAtom } from 'jotai'; import { useCallback, useState } from 'react'; -import { useParams } from 'react-router-dom'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; -import { pageSettingFamily, setPageModeAtom } from '../../../../atoms'; -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'; -import { toast } from '../../../../utils'; -import { HeaderDropDownButton } from '../../../pure/header-drop-down-button'; -import { usePageHelper } from '../../block-suite-page-list/utils'; -import { LanguageMenu } from './language-menu'; -import { MenuThemeModeSwitch } from './theme-mode-switch'; - -const CommonMenu = () => { - const content = ( -
{ - e.stopPropagation(); - }} - > - - -
- ); - return ( - - - - - - - - ); -}; +import { pageSettingFamily, setPageModeAtom } from '../../../atoms'; +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'; +import { toast } from '../../../utils'; +import { HeaderDropDownButton } from '../../pure/header-drop-down-button'; +import { usePageHelper } from '../block-suite-page-list/utils'; type PageMenuProps = { rename?: () => void; + pageId: string; }; -export const PageMenu = ({ rename }: PageMenuProps) => { +export const PageMenu = ({ rename, pageId }: PageMenuProps) => { 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); + ) as PageMeta; const [setting, setSetting] = useAtom(pageSettingFamily(pageId)); const mode = setting?.mode ?? 'page'; @@ -102,10 +67,10 @@ export const PageMenu = ({ rename }: PageMenuProps) => { ); }, [mode, setSetting, t]); const handleOnConfirm = useCallback(() => { - removeToTrash(pageMeta.id); + removeToTrash(pageId); toast(t['Moved to Trash']()); setOpenConfirm(false); - }, [pageMeta.id, removeToTrash, t]); + }, [pageId, removeToTrash, t]); const menuItemStyle = { padding: '4px 12px', }; @@ -237,7 +202,3 @@ export const PageMenu = ({ rename }: PageMenuProps) => { ); }; -export const EditorOptionMenu = () => { - const { pageId } = useParams(); - return pageId ? : ; -}; diff --git a/apps/core/src/components/blocksuite/block-suite-header-title/styles.css.ts b/apps/core/src/components/blocksuite/block-suite-header-title/styles.css.ts new file mode 100644 index 000000000..a50054590 --- /dev/null +++ b/apps/core/src/components/blocksuite/block-suite-header-title/styles.css.ts @@ -0,0 +1,31 @@ +import { style } from '@vanilla-extract/css'; + +export const headerTitleContainer = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexGrow: 1, + position: 'relative', + width: '100%', +}); + +export const titleEditButton = style({ + flexGrow: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +export const titleInput = style({ + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + margin: 'auto', + width: '100%', + height: '100%', +}); +export const shadowTitle = style({ + visibility: 'hidden', +}); diff --git a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/animation-data/edgeless-hover.json b/apps/core/src/components/blocksuite/block-suite-mode-switch/animation-data/edgeless-hover.json similarity index 100% rename from apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/animation-data/edgeless-hover.json rename to apps/core/src/components/blocksuite/block-suite-mode-switch/animation-data/edgeless-hover.json diff --git a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/animation-data/page-hover.json b/apps/core/src/components/blocksuite/block-suite-mode-switch/animation-data/page-hover.json similarity index 100% rename from apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/animation-data/page-hover.json rename to apps/core/src/components/blocksuite/block-suite-mode-switch/animation-data/page-hover.json diff --git a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/index.tsx b/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx similarity index 95% rename from apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/index.tsx rename to apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx index 84113b626..10d4837aa 100644 --- a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/index.tsx +++ b/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx @@ -6,9 +6,9 @@ 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 { pageSettingFamily } from '../../../atoms'; +import type { BlockSuiteWorkspace } from '../../../shared'; +import { toast } from '../../../utils'; import { StyledEditorModeSwitch, StyledKeyboardItem } from './style'; import { EdgelessSwitchItem, PageSwitchItem } from './switch-items'; diff --git a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/style.ts b/apps/core/src/components/blocksuite/block-suite-mode-switch/style.ts similarity index 100% rename from apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/style.ts rename to apps/core/src/components/blocksuite/block-suite-mode-switch/style.ts diff --git a/apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/switch-items.tsx b/apps/core/src/components/blocksuite/block-suite-mode-switch/switch-items.tsx similarity index 100% rename from apps/core/src/components/blocksuite/workspace-header/editor-mode-switch/switch-items.tsx rename to apps/core/src/components/blocksuite/block-suite-mode-switch/switch-items.tsx diff --git a/apps/core/src/components/blocksuite/workspace-header/download-tips.tsx b/apps/core/src/components/blocksuite/workspace-header/download-tips.tsx deleted file mode 100644 index e4dfd3e6b..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/download-tips.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 ; -}; -export default DownloadClientTip; diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/language-menu.tsx b/apps/core/src/components/blocksuite/workspace-header/header-right-items/language-menu.tsx deleted file mode 100644 index babd763ac..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/language-menu.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { displayFlex, Menu, MenuItem, styled } from '@affine/component'; -import { LOCALES } from '@affine/i18n'; -import { useI18N } from '@affine/i18n'; -import { ArrowDownSmallIcon, LanguageIcon } from '@blocksuite/icons'; -import { Button } from '@toeverything/components/button'; -import type { ReactElement } from 'react'; -import { useCallback } from 'react'; - -const LanguageMenuContent = () => { - const i18n = useI18N(); - const changeLanguage = useCallback( - (event: string) => { - i18n.changeLanguage(event).catch(err => { - console.error(err); - }); - }, - [i18n] - ); - - return ( - <> - {LOCALES.map(option => { - return ( - { - changeLanguage(option.tag); - }} - > - {option.originalName} - - ); - })} - - ); -}; - -export const LanguageMenu = () => { - const i18n = useI18N(); - - const currentLanguage = LOCALES.find(item => item.tag === i18n.language); - - return ( - - - - - - ) as ReactElement} - placement="bottom" - trigger="click" - disablePortal={true} - > - - - - } - iconPosition="end" - data-testid="language-menu-button" - > - - {currentLanguage?.originalName} - - - - - - ); -}; - -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)', - }; -}); diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/icons.tsx b/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/icons.tsx deleted file mode 100644 index a1b5e1bc8..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/icons.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { CSSProperties, DOMAttributes } from 'react'; -type IconProps = { - style?: CSSProperties; -} & DOMAttributes; - -export const MoonIcon = ({ style = {}, ...props }: IconProps) => { - return ( - - - - ); -}; - -export const SunIcon = ({ style = {}, ...props }: IconProps) => { - return ( - - - - ); -}; diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/index.tsx b/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/index.tsx deleted file mode 100644 index 369a3b16f..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -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 ( - - - - - - - - - - - { - setTheme('light'); - }} - > - {t['light']()} - - - { - setTheme('dark'); - }} - > - {t['dark']()} - - - { - setTheme('system'); - }} - > - {t['system']()} - - - - ); -}; - -export default MenuThemeModeSwitch; diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/style.ts b/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/style.ts deleted file mode 100644 index 229dd2de8..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/theme-mode-switch/style.ts +++ /dev/null @@ -1,120 +0,0 @@ -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; - //}, - `; -}); diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/user-avatar.tsx b/apps/core/src/components/blocksuite/workspace-header/header-right-items/user-avatar.tsx deleted file mode 100644 index 7754fd912..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/user-avatar.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Menu, MenuItem } from '@affine/component'; -import { Logo1Icon, SignOutIcon } from '@blocksuite/icons'; -import type { CSSProperties } from 'react'; -import { forwardRef } from 'react'; - -const EditMenu = ( - }> - Sign Out - -); - -export const UserAvatar = () => { - // fixme: cloud regression - const user: any = null; - return ( - - {user ? ( - - ) : ( - - )} - - ); -}; - -interface WorkspaceAvatarProps { - size: number; - name?: string; - avatar?: string; - style?: CSSProperties; -} - -export const WorkspaceAvatar = forwardRef( - function WorkspaceAvatar(props, ref) { - const size = props.size || 20; - const sizeStr = size + 'px'; - - return ( - <> - {props.avatar ? ( -
- - - -
- ) : ( -
- {props.name ? ( - props.name.substring(0, 1) - ) : ( - - )} -
- )} - - ); - } -); -export default UserAvatar; diff --git a/apps/core/src/components/blocksuite/workspace-header/header.tsx b/apps/core/src/components/blocksuite/workspace-header/header.tsx deleted file mode 100644 index 107b068ec..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/header.tsx +++ /dev/null @@ -1,258 +0,0 @@ -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 { - addCleanup, - pluginHeaderItemAtom, -} from '@toeverything/infra/__internal__/plugin'; -import clsx from 'clsx'; -import { useAtom, useAtomValue } from 'jotai'; -import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; -import { - forwardRef, - startTransition, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; - -import { guideDownloadClientTipAtom } from '../../../atoms/guide'; -import { currentModeAtom } from '../../../atoms/mode'; -import type { AffineOfficialWorkspace } from '../../../shared'; -import DownloadClientTip from './download-tips'; -import { EditorOptionMenu } from './header-right-items/editor-option-menu'; -import * as styles from './styles.css'; -import { OSWarningMessage, shouldShowWarning } from './utils'; - -export interface BaseHeaderProps< - Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace, -> { - workspace: Workspace; - currentPage: Page | null; - isPublic: boolean; - leftSlot?: ReactNode; -} - -export enum HeaderRightItemName { - EditorOptionMenu = 'editorOptionMenu', -} - -interface HeaderItem { - Component: (props: BaseHeaderProps) => ReactElement; - // todo: public workspace should be one of the flavour - availableWhen: ( - workspace: AffineOfficialWorkspace, - currentPage: Page | null, - status: { - isPublic: boolean; - } - ) => boolean; -} - -const HeaderRightItems: Record = { - [HeaderRightItemName.EditorOptionMenu]: { - Component: EditorOptionMenu, - availableWhen: () => { - return false; - }, - }, -}; - -const WindowsAppControls = () => { - 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 ( -
- - - -
- ); -}; - -const PluginHeader = () => { - const headerItem = useAtomValue(pluginHeaderItemAtom); - const pluginsRef = useRef([]); - - return ( -
{ - if (root) { - Object.entries(headerItem).forEach(([pluginName, create]) => { - if (pluginsRef.current.includes(pluginName)) { - return; - } - pluginsRef.current.push(pluginName); - const div = document.createElement('div'); - div.setAttribute('plugin-id', pluginName); - startTransition(() => { - const cleanup = create(div); - root.appendChild(div); - addCleanup(pluginName, () => { - pluginsRef.current = pluginsRef.current.filter( - name => name !== pluginName - ); - root.removeChild(div); - cleanup(); - }); - }); - }); - } - }, - [headerItem] - )} - /> - ); -}; - -export interface HeaderProps - extends BaseHeaderProps, - HTMLAttributes { - children?: ReactNode; -} - -export const Header = forwardRef((props, ref) => { - const [showWarning, setShowWarning] = useState(false); - const [showDownloadTip, setShowDownloadTip] = useAtom( - guideDownloadClientTipAtom - ); - useEffect(() => { - setShowWarning(shouldShowWarning()); - }, []); - const open = useAtomValue(appSidebarOpenAtom); - const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); - - const mode = useAtomValue(currentModeAtom); - const isWindowsDesktop = globalThis.platform === 'win32' && isDesktop; - - return ( -
- {showDownloadTip ? ( - { - setShowDownloadTip(false); - localStorage.setItem('affine-is-dt-hide', '1'); - }} - /> - ) : ( - } - onClose={() => { - setShowWarning(false); - }} - /> - )} -
-
-
{!open && }
-
- {props.leftSlot} -
-
- - {props.children} -
- - {useMemo(() => { - return Object.entries(HeaderRightItems).map( - ([name, { availableWhen, Component }]) => { - if ( - availableWhen(props.workspace, props.currentPage, { - isPublic: props.isPublic, - }) - ) { - return ( - - ); - } - return null; - } - ); - }, [props])} -
- {isWindowsDesktop ? : null} -
-
- ); -}); - -Header.displayName = 'Header'; diff --git a/apps/core/src/components/blocksuite/workspace-header/index.tsx b/apps/core/src/components/blocksuite/workspace-header/index.tsx deleted file mode 100644 index e5a2d2df5..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/index.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { assertExists } from '@blocksuite/global/utils'; -import { - useBlockSuitePageMeta, - usePageMetaHelper, -} from '@toeverything/hooks/use-block-suite-page-meta'; -import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; -import { useCallback, useRef, useState } from 'react'; - -import { EditorModeSwitch } from './editor-mode-switch'; -import type { BaseHeaderProps } from './header'; -import { Header } from './header'; -import { PageMenu } from './header-right-items/editor-option-menu'; -import * as styles from './styles.css'; - -export interface WorkspaceHeaderProps - extends BaseHeaderProps, - HTMLAttributes { - children?: ReactNode; -} - -export const BlockSuiteEditorHeader = ( - props: WorkspaceHeaderProps -): ReactElement => { - const { workspace, currentPage, children, isPublic } = props; - // fixme(himself65): remove this atom and move it to props - const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find( - meta => meta.id === currentPage?.id - ); - const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace); - const [isEditable, setIsEditable] = useState(false); - const inputRef = useRef(null); - const handleClick = useCallback(() => { - if (isEditable) { - setIsEditable(!isEditable); - const value = inputRef.current?.value; - if (value !== pageMeta?.title && currentPage) { - pageTitleMeta.setPageTitle(currentPage?.id, value || ''); - } - } else { - setIsEditable(!isEditable); - } - }, [currentPage, isEditable, pageMeta?.title, pageTitleMeta]); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === 'Escape') { - handleClick(); - } - }, - [handleClick] - ); - const headerRef = useRef(null); - assertExists(pageMeta); - const title = pageMeta?.title; - - return ( -
- {children} - {!isPublic && currentPage && ( -
-
-
- -
-
- {isEditable ? ( -
- -
- ) : ( - - {title || 'Untitled'} - - )} -
-
- -
-
-
- )} -
- ); -}; - -BlockSuiteEditorHeader.displayName = 'BlockSuiteEditorHeader'; diff --git a/apps/core/src/components/blocksuite/workspace-header/styles.css.ts b/apps/core/src/components/blocksuite/workspace-header/styles.css.ts deleted file mode 100644 index cfc251c91..000000000 --- a/apps/core/src/components/blocksuite/workspace-header/styles.css.ts +++ /dev/null @@ -1,315 +0,0 @@ -import type { ComplexStyleRule } from '@vanilla-extract/css'; -import { createContainer, style } from '@vanilla-extract/css'; - -export const headerVanillaContainer = createContainer(); - -export const headerContainer = style({ - height: 'auto', - 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({ - containerName: headerVanillaContainer, - containerType: 'inline-size', - flexShrink: 0, - minHeight: '52px', - width: '100%', - padding: '8px 20px', - display: 'grid', - gridTemplateColumns: '1fr auto 1fr', - alignItems: 'center', - background: 'var(--affine-background-primary-color)', - zIndex: 99, - position: 'relative', - selectors: { - '&[data-is-page-list="true"], &[data-is-edgeless="true"]': { - borderBottom: `1px solid var(--affine-border-color)`, - }, - }, - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - alignItems: 'start', - }, - }, -}); - -export const titleContainer = style({ - width: '100%', - height: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - alignContent: 'unset', - fontSize: 'var(--affine-font-base)', - ['WebkitAppRegion' as string]: 'no-drag', - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - alignItems: 'start', - paddingTop: '2px', - }, - }, -}); - -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 pageTitle = style({ - maxWidth: '600px', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - transition: 'width .15s', - cursor: 'pointer', - '@container': { - [`${headerVanillaContainer} (max-width: 1920px)`]: { - maxWidth: '800px', - }, - [`${headerVanillaContainer} (max-width: 1300px)`]: { - maxWidth: '400px', - }, - [`${headerVanillaContainer} (max-width: 768px)`]: { - maxWidth: '220px', - }, - }, -}); -export const titleWrapper = style({ - position: 'relative', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); -export const headerLeftSide = style({ - display: 'flex', - alignItems: 'center', - transition: 'all .15s', -}); -export const headerLeftSideColumn = style({ - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - flexDirection: 'column', - alignItems: 'flex-start', - height: '68px', - }, - }, -}); -export const headerLeftSideItem = style({ - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - position: 'absolute', - left: '0', - bottom: '8px', - }, - }, -}); -export const headerLeftSideOpen = style({ - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - marginLeft: '20px', - }, - }, -}); -export const headerRightSide = style({ - height: '100%', - display: 'flex', - alignItems: 'center', - gap: '12px', - zIndex: 1, - marginLeft: '20px', - justifyContent: 'flex-end', - transition: 'all .15s', -}); - -export const headerRightSideColumn = style({ - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - position: 'absolute', - height: 'auto', - right: '0', - bottom: '8px', - marginRight: '18px', - }, - }, -}); -export const headerRightSideWindow = style({ - marginRight: '140px', -}); -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({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); - -export const searchArrowWrapper = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginLeft: '4px', -}); - -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', - alignItems: 'center', - width: '100%', - height: '100%', - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - alignItems: 'flex-start', - marginTop: '8px', - }, - }, -}); -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%', - position: 'absolute', - right: '14px', -}); - -export const windowAppControl = style({ - WebkitAppRegion: 'no-drag', - cursor: 'pointer', - display: 'inline-flex', - width: '51px', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '0', - selectors: { - '&[data-type="close"]': { - width: '56px', - paddingRight: '5px', - marginRight: '-12px', - }, - '&[data-type="close"]:hover': { - background: 'var(--affine-windows-close-button)', - color: 'var(--affine-pure-white)', - }, - '&:hover': { - background: 'var(--affine-hover-color)', - }, - }, - '@container': { - [`${headerVanillaContainer} (max-width: 900px)`]: { - height: '50px', - paddingTop: '0', - }, - }, -} as ComplexStyleRule); - -export const pluginHeaderItems = style({ - display: 'flex', - gap: '12px', - alignItems: 'center', - height: '100%', -}); diff --git a/apps/core/src/components/page-detail-editor.tsx b/apps/core/src/components/page-detail-editor.tsx index 896856528..af6b310a9 100644 --- a/apps/core/src/components/page-detail-editor.tsx +++ b/apps/core/src/components/page-detail-editor.tsx @@ -23,9 +23,9 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { pageSettingFamily } from '../atoms'; import { fontStyleOptions, useAppSetting } from '../atoms/settings'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; -import { TrashButtonGroup } from './blocksuite/workspace-header/header-right-items/trash-button-group'; import * as styles from './page-detail-editor.css'; import { pluginContainer } from './page-detail-editor.css'; +import { TrashButtonGroup } from './pure/trash-button-group'; export interface PageDetailEditorProps { isPublic?: boolean; diff --git a/apps/core/src/components/pure/header/index.tsx b/apps/core/src/components/pure/header/index.tsx new file mode 100644 index 000000000..70fb55c0a --- /dev/null +++ b/apps/core/src/components/pure/header/index.tsx @@ -0,0 +1,150 @@ +import { Wrapper } from '@affine/component'; +import { + appSidebarFloatingAtom, + appSidebarOpenAtom, + SidebarSwitch, +} from '@affine/component/app-sidebar'; +import { isDesktop } from '@affine/env/constant'; +import clsx from 'clsx'; +import { useAtomValue } from 'jotai'; +import throttle from 'lodash.throttle'; +import type { MutableRefObject, ReactNode } from 'react'; +import { useEffect, useRef, useState } from 'react'; + +import * as style from './style.css'; +import { TopTip } from './top-tip'; +import { WindowsAppControls } from './windows-app-controls'; +interface HeaderPros { + left?: ReactNode; + right?: ReactNode; + center?: ReactNode; +} + +const useIsTinyScreen = ({ + mainContainer, + leftDoms, + centerDom, + rightDoms, +}: { + mainContainer: HTMLElement; + leftDoms: MutableRefObject[]; + centerDom: MutableRefObject; + rightDoms: MutableRefObject[]; +}) => { + const [isTinyScreen, setIsTinyScreen] = useState(false); + + useEffect(() => { + const handleResize = throttle(() => { + if (!centerDom.current) { + return; + } + const leftTotalWidth = leftDoms.reduce((accWidth, dom) => { + return accWidth + (dom.current?.clientWidth || 0); + }, 0); + + const rightTotalWidth = rightDoms.reduce((accWidth, dom) => { + return accWidth + (dom.current?.clientWidth || 0); + }, 0); + + const containerRect = mainContainer.getBoundingClientRect(); + const centerRect = centerDom.current.getBoundingClientRect(); + + const offset = isTinyScreen ? 50 : 0; + if ( + leftTotalWidth + containerRect.left >= centerRect.left - offset || + containerRect.right - centerRect.right <= rightTotalWidth + offset + ) { + setIsTinyScreen(true); + } else { + setIsTinyScreen(false); + } + }, 100); + + handleResize(); + + const resizeObserver = new ResizeObserver(() => { + handleResize(); + }); + + resizeObserver.observe(mainContainer); + }, [centerDom, isTinyScreen, leftDoms, mainContainer, rightDoms]); + + return isTinyScreen; +}; + +// The Header component is used to solve the following problems +// 1. Manage layout issues independently of page or business logic +// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed +export const Header = ({ left, center, right }: HeaderPros) => { + const sidebarSwitchRef = useRef(null); + const leftSlotRef = useRef(null); + const centerSlotRef = useRef(null); + const rightSlotRef = useRef(null); + const windowControlsRef = useRef(null); + + const isTinyScreen = useIsTinyScreen({ + mainContainer: document.querySelector('.main-container') || document.body, + leftDoms: [sidebarSwitchRef, leftSlotRef], + centerDom: centerSlotRef, + rightDoms: [rightSlotRef, windowControlsRef], + }); + + const isWindowsDesktop = globalThis.platform === 'win32' && isDesktop; + const open = useAtomValue(appSidebarOpenAtom); + const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); + return ( + <> + +
+
+
+
+ {!open && ( + + + + )} +
+
+
+
{left}
+
+
+
+ {center} +
+
+
+
+ {isWindowsDesktop ? : null} +
+
+
+
{right}
+
+
+
+ + ); +}; diff --git a/apps/core/src/components/pure/header/style.css.tsx b/apps/core/src/components/pure/header/style.css.tsx new file mode 100644 index 000000000..a07c70d55 --- /dev/null +++ b/apps/core/src/components/pure/header/style.css.tsx @@ -0,0 +1,111 @@ +import type { ComplexStyleRule } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; + +export const header = style({ + display: 'flex', + justifyContent: 'space-between', + position: 'relative', + padding: '0 16px', + minHeight: '52px', + borderBottom: '1px solid var(--affine-border-color)', + selectors: { + '&[data-sidebar-floating="false"]': { + WebkitAppRegion: 'drag', + }, + }, + '@media': { + print: { + display: 'none', + }, + }, + ':has([data-popper-placement])': { + WebkitAppRegion: 'no-drag', + }, +} as ComplexStyleRule); + +export const headerItem = style({ + minHeight: '32px', + display: 'flex', + alignItems: 'center', + flexShrink: 0, + selectors: { + '&.top-item': { + height: '52px', + }, + '&.left': { + justifyContent: 'left', + }, + '&.right': { + justifyContent: 'right', + }, + }, +}); + +export const headerCenter = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '52px', + flexShrink: 0, + maxWidth: '60%', + position: 'absolute', + transform: 'translateX(-50%)', + left: '50%', + zIndex: 1, + selectors: { + '&.is-window': { + maxWidth: '50%', + }, + '&.is-window.has-min-width': { + minWidth: '400px', + }, + '&.shadow': { + position: 'static', + visibility: 'hidden', + }, + }, +}); + +export const headerSideContainer = style({ + display: 'flex', + flexShrink: 0, + alignItems: 'center', + selectors: { + '&.right': { + flexDirection: 'row-reverse', + }, + '&.block': { + display: 'block', + }, + }, +}); + +export const windowAppControlsWrapper = style({ + display: 'flex', + marginLeft: '20px', +}); + +export const windowAppControl = style({ + WebkitAppRegion: 'no-drag', + cursor: 'pointer', + display: 'inline-flex', + width: '52px', + height: '52px', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '0', + selectors: { + '&[data-type="close"]': { + width: '56px', + paddingRight: '5px', + marginRight: '-12px', + }, + '&[data-type="close"]:hover': { + background: 'var(--affine-windows-close-button)', + color: 'var(--affine-pure-white)', + }, + '&:hover': { + background: 'var(--affine-hover-color)', + }, + }, +} as ComplexStyleRule); diff --git a/apps/core/src/components/blocksuite/workspace-header/utils.tsx b/apps/core/src/components/pure/header/top-tip.tsx similarity index 57% rename from apps/core/src/components/blocksuite/workspace-header/utils.tsx rename to apps/core/src/components/pure/header/top-tip.tsx index e3a704d70..992517455 100644 --- a/apps/core/src/components/blocksuite/workspace-header/utils.tsx +++ b/apps/core/src/components/pure/header/top-tip.tsx @@ -1,11 +1,16 @@ +import { BrowserWarning } from '@affine/component/affine-banner'; +import { DownloadTips } from '@affine/component/affine-banner'; import { isDesktop } from '@affine/env/constant'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { useAtom } from 'jotai'; import { useEffect, useState } from 'react'; +import { guideDownloadClientTipAtom } from '../../../atoms/guide'; + const minimumChromeVersion = 102; -export const shouldShowWarning = () => { +const shouldShowWarning = () => { if (isDesktop) { // even though desktop has compatibility issues, // we don't want to show the warning @@ -22,7 +27,7 @@ export const shouldShowWarning = () => { } }; -export const OSWarningMessage = () => { +const OSWarningMessage = () => { const t = useAFFiNEI18N(); const [notChrome, setNotChrome] = useState(false); const [notGoodVersion, setNotGoodVersion] = useState(false); @@ -49,3 +54,34 @@ export const OSWarningMessage = () => { } return null; }; +export const TopTip = () => { + const [showWarning, setShowWarning] = useState(false); + const [showDownloadTip, setShowDownloadTip] = useAtom( + guideDownloadClientTipAtom + ); + + useEffect(() => { + setShowWarning(shouldShowWarning()); + }, []); + + if (showDownloadTip && isDesktop) { + return ( + { + setShowDownloadTip(false); + localStorage.setItem('affine-is-dt-hide', '1'); + }} + /> + ); + } + + return ( + } + onClose={() => { + setShowWarning(false); + }} + /> + ); +}; diff --git a/apps/core/src/components/pure/header/windows-app-controls.tsx b/apps/core/src/components/pure/header/windows-app-controls.tsx new file mode 100644 index 000000000..6a6ea0ca5 --- /dev/null +++ b/apps/core/src/components/pure/header/windows-app-controls.tsx @@ -0,0 +1,51 @@ +import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons'; +import { useCallback } from 'react'; + +import * as style from './style.css'; + +export const WindowsAppControls = () => { + 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 ( +
+ + + +
+ ); +}; diff --git a/apps/core/src/components/pure/plugin-header/index.tsx b/apps/core/src/components/pure/plugin-header/index.tsx new file mode 100644 index 000000000..3484f3b73 --- /dev/null +++ b/apps/core/src/components/pure/plugin-header/index.tsx @@ -0,0 +1,44 @@ +import { + addCleanup, + pluginHeaderItemAtom, +} from '@toeverything/infra/__internal__/plugin'; +import { useAtomValue } from 'jotai'; +import { startTransition, useCallback, useRef } from 'react'; + +import * as styles from './styles.css'; +export const PluginHeader = () => { + const headerItem = useAtomValue(pluginHeaderItemAtom); + const pluginsRef = useRef([]); + + return ( +
{ + if (root) { + Object.entries(headerItem).forEach(([pluginName, create]) => { + if (pluginsRef.current.includes(pluginName)) { + return; + } + pluginsRef.current.push(pluginName); + const div = document.createElement('div'); + div.setAttribute('plugin-id', pluginName); + startTransition(() => { + const cleanup = create(div); + root.appendChild(div); + addCleanup(pluginName, () => { + pluginsRef.current = pluginsRef.current.filter( + name => name !== pluginName + ); + root.removeChild(div); + cleanup(); + }); + }); + }); + } + }, + [headerItem] + )} + /> + ); +}; diff --git a/apps/core/src/components/pure/plugin-header/styles.css.ts b/apps/core/src/components/pure/plugin-header/styles.css.ts new file mode 100644 index 000000000..254ef6618 --- /dev/null +++ b/apps/core/src/components/pure/plugin-header/styles.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const pluginHeaderItems = style({ + display: 'flex', + gap: '12px', + alignItems: 'center', + height: '100%', +}); diff --git a/apps/core/src/components/blocksuite/workspace-header/header-right-items/trash-button-group.tsx b/apps/core/src/components/pure/trash-button-group/index.tsx similarity index 88% rename from apps/core/src/components/blocksuite/workspace-header/header-right-items/trash-button-group.tsx rename to apps/core/src/components/pure/trash-button-group/index.tsx index b10aa4ff1..c3fade095 100644 --- a/apps/core/src/components/blocksuite/workspace-header/header-right-items/trash-button-group.tsx +++ b/apps/core/src/components/pure/trash-button-group/index.tsx @@ -8,10 +8,11 @@ import { currentPageIdAtom } from '@toeverything/infra/atom'; 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'; +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'; import { buttonContainer, group } from './styles.css'; + export const TrashButtonGroup = () => { // fixme(himself65): remove these hooks ASAP const [workspace] = useCurrentWorkspace(); @@ -33,7 +34,7 @@ export const TrashButtonGroup = () => {
@@ -215,6 +201,6 @@ export const CollectionList = ({ onClose={closeUpdateCollectionModal} onConfirm={onConfirm} > -
+ ); }; diff --git a/tests/kit/utils/filter.ts b/tests/kit/utils/filter.ts index 9bac0d712..6282917ac 100644 --- a/tests/kit/utils/filter.ts +++ b/tests/kit/utils/filter.ts @@ -20,7 +20,7 @@ const monthNames = [ export const createFirstFilter = async (page: Page, name: string) => { await page - .locator('[data-testid="editor-header-items"]') + .locator('[data-testid="header"]') .locator('button', { hasText: 'Filter' }) .click(); await page diff --git a/tests/kit/utils/page-logic.ts b/tests/kit/utils/page-logic.ts index 07f056f6c..aab8ffacd 100644 --- a/tests/kit/utils/page-logic.ts +++ b/tests/kit/utils/page-logic.ts @@ -52,7 +52,7 @@ export const createLinkedPage = async (page: Page, pageName?: string) => { export async function clickPageMoreActions(page: Page) { return page - .getByTestId('editor-header-items') + .getByTestId('header') .getByTestId('header-dropDownButton') .click(); } diff --git a/yarn.lock b/yarn.lock index bb62d07c1..2bb7d7a8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -247,6 +247,7 @@ __metadata: "@svgr/webpack": ^8.0.1 "@swc/core": ^1.3.76 "@toeverything/components": ^0.0.10 + "@types/lodash.throttle": ^4.1.7 "@types/webpack-env": ^1.18.1 async-call-rpc: ^6.3.1 cmdk: ^0.2.0 @@ -261,6 +262,7 @@ __metadata: jotai: ^2.3.1 jotai-devtools: ^0.6.1 lit: ^2.8.0 + lodash.throttle: ^4.1.1 lottie-web: ^5.12.2 mini-css-extract-plugin: ^2.7.6 next-themes: ^0.2.1 @@ -12040,6 +12042,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.throttle@npm:^4.1.7": + version: 4.1.7 + resolution: "@types/lodash.throttle@npm:4.1.7" + dependencies: + "@types/lodash": "*" + checksum: 6e1b3836488fecbdc537b6ad9b3fe4855c7336b0fa388773cd57d486619f565a48cabc04b28677fd3819be3f2d13d2bb8f9d4428aa5632885c86cb99729bfd69 + languageName: node + linkType: hard + "@types/lodash@npm:*, @types/lodash@npm:^4.14.167, @types/lodash@npm:^4.14.178, @types/lodash@npm:^4.14.182, @types/lodash@npm:^4.14.191": version: 4.14.197 resolution: "@types/lodash@npm:4.14.197" @@ -23743,6 +23754,13 @@ __metadata: languageName: node linkType: hard +"lodash.throttle@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.throttle@npm:4.1.1" + checksum: 129c0a28cee48b348aef146f638ef8a8b197944d4e9ec26c1890c19d9bf5a5690fe11b655c77a4551268819b32d27f4206343e30c78961f60b561b8608c8c805 + languageName: node + linkType: hard + "lodash.truncate@npm:^4.4.2": version: 4.4.2 resolution: "lodash.truncate@npm:4.4.2"