mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
Compare commits
4 Commits
v0.20.0-ca
...
v0.17.1-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c836df730 | ||
|
|
1d8b1aa978 | ||
|
|
b9e5a72cc1 | ||
|
|
17182fc763 |
@@ -6,6 +6,7 @@ export const resizeHandleVerticalPadding = createVar(
|
||||
'resize-handle-vertical-padding'
|
||||
);
|
||||
export const animationTimeout = createVar();
|
||||
|
||||
export const root = style({
|
||||
vars: {
|
||||
[panelWidthVar]: '256px',
|
||||
@@ -15,23 +16,28 @@ export const root = style({
|
||||
width: panelWidthVar,
|
||||
minWidth: panelWidthVar,
|
||||
height: '100%',
|
||||
zIndex: 4,
|
||||
transform: 'translateX(0)',
|
||||
maxWidth: '50%',
|
||||
selectors: {
|
||||
'&[data-is-floating="true"]': {
|
||||
position: 'absolute',
|
||||
width: `calc(${panelWidthVar})`,
|
||||
zIndex: 4,
|
||||
},
|
||||
'&[data-open="true"]': {
|
||||
maxWidth: '50%',
|
||||
},
|
||||
'&[data-open="false"][data-handle-position="right"]': {
|
||||
marginLeft: `calc(${panelWidthVar} * -1)`,
|
||||
},
|
||||
'&[data-open="false"][data-handle-position="left"]': {
|
||||
marginRight: `calc(${panelWidthVar} * -1)`,
|
||||
},
|
||||
'&[data-open="false"][data-handle-position="right"],&[data-is-floating="true"][data-handle-position="right"]':
|
||||
{
|
||||
marginLeft: `calc(${panelWidthVar} * -1)`,
|
||||
},
|
||||
'&[data-open="false"][data-handle-position="left"],&[data-is-floating="true"][data-handle-position="left"]':
|
||||
{
|
||||
marginRight: `calc(${panelWidthVar} * -1)`,
|
||||
},
|
||||
'&[data-open="true"][data-handle-position="right"][data-is-floating="true"]':
|
||||
{
|
||||
transform: `translateX(${panelWidthVar})`,
|
||||
},
|
||||
'&[data-open="true"][data-handle-position="left"][data-is-floating="true"]':
|
||||
{
|
||||
transform: `translateX(-${panelWidthVar})`,
|
||||
},
|
||||
'&[data-enable-animation="true"]': {
|
||||
transition: `margin-left ${animationTimeout} .05s, margin-right ${animationTimeout} .05s, width ${animationTimeout} .05s`,
|
||||
transition: `margin-left ${animationTimeout}, margin-right ${animationTimeout}, transform ${animationTimeout}, background ${animationTimeout}`,
|
||||
},
|
||||
'&[data-transition-state="exited"]': {
|
||||
// avoid focus on hidden panel
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
|
||||
import { useTransition } from 'react-transition-state';
|
||||
|
||||
import * as styles from './resize-panel.css';
|
||||
@@ -60,48 +52,53 @@ const ResizeHandle = ({
|
||||
...rest
|
||||
}: ResizeHandleProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const onResizeStart = useCallback(() => {
|
||||
let resized = false;
|
||||
const panelContainer = ref.current?.parentElement;
|
||||
assertExists(
|
||||
panelContainer,
|
||||
'parent element not found for resize indicator'
|
||||
);
|
||||
|
||||
const { left: anchorLeft, right: anchorRight } =
|
||||
panelContainer.getBoundingClientRect();
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const onResizeStart = useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
let resized = false;
|
||||
const panelContainer = ref.current?.parentElement;
|
||||
if (!panelContainer) return;
|
||||
const newWidth = Math.min(
|
||||
maxWidth,
|
||||
Math.max(
|
||||
resizeHandlePos === 'right'
|
||||
? e.clientX - anchorLeft
|
||||
: anchorRight - e.clientX,
|
||||
minWidth
|
||||
)
|
||||
);
|
||||
onWidthChange(newWidth);
|
||||
onResizing(true);
|
||||
resized = true;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
() => {
|
||||
// if not resized, toggle sidebar
|
||||
if (!resized) {
|
||||
onOpen(false);
|
||||
}
|
||||
onResizing(false);
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}, [maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen]);
|
||||
// add cursor style to body
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
const { left: anchorLeft, right: anchorRight } =
|
||||
panelContainer.getBoundingClientRect();
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
if (!panelContainer) return;
|
||||
const newWidth = Math.min(
|
||||
maxWidth,
|
||||
Math.max(
|
||||
resizeHandlePos === 'right'
|
||||
? e.clientX - anchorLeft
|
||||
: anchorRight - e.clientX,
|
||||
minWidth
|
||||
)
|
||||
);
|
||||
onWidthChange(newWidth);
|
||||
onResizing(true);
|
||||
resized = true;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
() => {
|
||||
// if not resized, toggle sidebar
|
||||
if (!resized) {
|
||||
onOpen(false);
|
||||
}
|
||||
onResizing(false);
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.body.style.cursor = '';
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
},
|
||||
[maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -125,17 +122,6 @@ const ResizeHandle = ({
|
||||
);
|
||||
};
|
||||
|
||||
// delay initial animation to avoid flickering
|
||||
function useEnableAnimation() {
|
||||
const [enable, setEnable] = useState(false);
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
setEnable(true);
|
||||
}, 500);
|
||||
}, []);
|
||||
return enable;
|
||||
}
|
||||
|
||||
const animationTimeout = 300;
|
||||
|
||||
export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||
@@ -148,7 +134,7 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||
maxWidth,
|
||||
width,
|
||||
floating,
|
||||
enableAnimation: _enableAnimation = true,
|
||||
enableAnimation = true,
|
||||
open,
|
||||
unmountOnExit,
|
||||
onOpen,
|
||||
@@ -161,7 +147,6 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const enableAnimation = useEnableAnimation() && _enableAnimation;
|
||||
const safeWidth = Math.min(maxWidth, Math.max(minWidth, width));
|
||||
const [{ status }, toggle] = useTransition({
|
||||
timeout: animationTimeout,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { SidebarIcon } from '@blocksuite/icons/rc';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
import { appSidebarOpenAtom } from '../components/app-sidebar';
|
||||
import type { AppSidebarService } from '../modules/app-sidebar';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
export function registerAffineLayoutCommands({
|
||||
t,
|
||||
store,
|
||||
appSidebarService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
appSidebarService: AppSidebarService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@@ -20,7 +19,7 @@ export function registerAffineLayoutCommands({
|
||||
category: 'affine:layout',
|
||||
icon: <SidebarIcon />,
|
||||
label: () =>
|
||||
store.get(appSidebarOpenAtom)
|
||||
appSidebarService.sidebar.open$.value
|
||||
? t['com.affine.cmdk.affine.left-sidebar.collapse']()
|
||||
: t['com.affine.cmdk.affine.left-sidebar.expand'](),
|
||||
|
||||
@@ -29,8 +28,7 @@ export function registerAffineLayoutCommands({
|
||||
},
|
||||
run() {
|
||||
track.$.navigationPanel.$.toggle();
|
||||
|
||||
store.set(appSidebarOpenAtom, v => !v);
|
||||
appSidebarService.sidebar.toggleSidebar();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
AppSidebarFallback,
|
||||
ShellAppSidebarFallback,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import clsx from 'clsx';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../components/hooks/affine/use-app-setting-helper';
|
||||
import { AppSidebarFallback, ShellAppSidebarFallback } from '../app-sidebar';
|
||||
import type { WorkspaceRootProps } from '../workspace';
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
|
||||
export const isMobile = !BUILD_CONFIG.isElectron && window.innerWidth < 768;
|
||||
|
||||
export const appSidebarOpenAtom = atomWithStorage(APP_SIDEBAR_OPEN, !isMobile);
|
||||
export const appSidebarFloatingAtom = atom(isMobile);
|
||||
|
||||
export const appSidebarResizingAtom = atom(false);
|
||||
export const appSidebarWidthAtom = atomWithStorage(
|
||||
'app-sidebar-width',
|
||||
248 /* px */
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { appSidebarOpenAtom } from '../../../components/app-sidebar';
|
||||
|
||||
export function useSwitchSidebarStatus() {
|
||||
const [isOpened, setOpened] = useAtom(appSidebarOpenAtom);
|
||||
|
||||
const onOpenChange = useCallback(() => {
|
||||
setOpened(open => !open);
|
||||
}, [setOpened]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
onOpenChange,
|
||||
isOpened,
|
||||
}),
|
||||
[isOpened, onOpenChange]
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
@@ -71,6 +72,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const appSidebarService = useService(AppSidebarService);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
||||
@@ -123,12 +125,12 @@ export function useRegisterWorkspaceCommands() {
|
||||
|
||||
// register AffineLayoutCommands
|
||||
useEffect(() => {
|
||||
const unsub = registerAffineLayoutCommands({ t, store });
|
||||
const unsub = registerAffineLayoutCommands({ t, appSidebarService });
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [store, t]);
|
||||
}, [appSidebarService, store, t]);
|
||||
|
||||
// register AffineCreationCommands
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
pushGlobalLoadingEventAtom,
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import { SidebarSwitch } from '@affine/core/modules/app-sidebar/views';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
@@ -40,7 +41,6 @@ import { WorkbenchService } from '../../modules/workbench';
|
||||
import { WorkspaceAIOnboarding } from '../affine/ai-onboarding';
|
||||
import { AppContainer } from '../affine/app-container';
|
||||
import { SyncAwareness } from '../affine/awareness';
|
||||
import { appSidebarResizingAtom, SidebarSwitch } from '../app-sidebar';
|
||||
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
||||
import { useSubscriptionNotifyReader } from '../hooks/affine/use-subscription-notify';
|
||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||
@@ -221,10 +221,8 @@ const WorkspaceLayoutUIContainer = ({ children }: PropsWithChildren) => {
|
||||
})
|
||||
);
|
||||
|
||||
const resizing = useAtomValue(appSidebarResizingAtom);
|
||||
|
||||
return (
|
||||
<AppContainer data-current-path={currentPath} resizing={resizing}>
|
||||
<AppContainer data-current-path={currentPath}>
|
||||
<LayoutComponent>{children}</LayoutComponent>
|
||||
</AppContainer>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { appSidebarFloatingAtom, appSidebarOpenAtom } from '../../app-sidebar';
|
||||
import * as style from './style.css';
|
||||
|
||||
interface HeaderPros {
|
||||
@@ -16,15 +16,10 @@ interface HeaderPros {
|
||||
// 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 open = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
return (
|
||||
<div
|
||||
className={clsx(style.header)}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
data-testid="header"
|
||||
>
|
||||
<div className={clsx(style.header)} data-open={open} data-testid="header">
|
||||
<div className={clsx(style.headerSideContainer)}>
|
||||
<div className={clsx(style.headerItem, 'left')}>
|
||||
<div>{left}</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { MenuItem } from '@affine/core/modules/app-sidebar/views';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import { ImportIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { MenuItem } from '../app-sidebar';
|
||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
|
||||
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AddPageButton,
|
||||
AppDownloadButton,
|
||||
AppSidebar,
|
||||
CategoryDivider,
|
||||
MenuItem,
|
||||
MenuLinkItem,
|
||||
QuickSearchInput,
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import { ExternalMenuLinkItem } from '@affine/core/modules/app-sidebar/views/menu-item/external-menu-link-item';
|
||||
import {
|
||||
ExplorerCollections,
|
||||
ExplorerFavorites,
|
||||
@@ -30,18 +42,6 @@ import type { MouseEvent, ReactElement } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { WorkbenchService } from '../../modules/workbench';
|
||||
import {
|
||||
AddPageButton,
|
||||
AppDownloadButton,
|
||||
AppSidebar,
|
||||
CategoryDivider,
|
||||
MenuItem,
|
||||
MenuLinkItem,
|
||||
QuickSearchInput,
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
} from '../app-sidebar';
|
||||
import { ExternalMenuLinkItem } from '../app-sidebar/menu-item/external-menu-link-item';
|
||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
import { WorkspaceNavigator } from '../workspace-selector';
|
||||
import ImportPage from './import-page';
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
useJournalInfoHelper,
|
||||
useJournalRouteHelper,
|
||||
} from '@affine/core/components/hooks/use-journal';
|
||||
import { MenuItem } from '@affine/core/modules/app-sidebar/views';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
@@ -12,8 +13,6 @@ import { TodayIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type MouseEvent } from 'react';
|
||||
|
||||
import { MenuItem } from '../app-sidebar';
|
||||
|
||||
interface AppSidebarJournalButtonProps {
|
||||
docCollection: DocCollection;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
useConfirmModal,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { MenuLinkItem } from '@affine/core/modules/app-sidebar/views';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
@@ -12,8 +13,6 @@ import {
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { MenuLinkItem } from '../app-sidebar';
|
||||
|
||||
export const TrashButton = () => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
|
||||
import { AppUpdaterButton } from '@affine/core/modules/app-sidebar/views';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { AppUpdaterButton } from '../app-sidebar';
|
||||
|
||||
const UpdaterButtonInner = () => {
|
||||
const appUpdater = useAppUpdater();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { cssVar, lightCssVariables } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const panelWidthVar = createVar('panel-width');
|
||||
|
||||
export const appStyle = style({
|
||||
width: '100%',
|
||||
@@ -9,9 +11,6 @@ export const appStyle = style({
|
||||
display: 'flex',
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
selectors: {
|
||||
'&[data-is-resizing="true"]': {
|
||||
cursor: 'col-resize',
|
||||
},
|
||||
'&.blur-background': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
@@ -51,7 +50,7 @@ export const mainContainerStyle = style({
|
||||
flex: 1,
|
||||
overflow: 'clip',
|
||||
maxWidth: '100%',
|
||||
transition: 'margin-left 0.2s ease',
|
||||
|
||||
selectors: {
|
||||
'&[data-client-border="true"]': {
|
||||
borderRadius: 6,
|
||||
@@ -65,9 +64,6 @@ export const mainContainerStyle = style({
|
||||
},
|
||||
},
|
||||
},
|
||||
'&[data-client-border="true"][data-side-bar-open="true"]': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
'&[data-client-border="true"][data-is-desktop="true"]': {
|
||||
marginTop: 0,
|
||||
},
|
||||
|
||||
@@ -6,22 +6,18 @@ import {
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { clsx } from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { appSidebarOpenAtom } from '../app-sidebar';
|
||||
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
|
||||
export type WorkspaceRootProps = PropsWithChildren<{
|
||||
resizing?: boolean;
|
||||
className?: string;
|
||||
useNoisyBackground?: boolean;
|
||||
useBlurBackground?: boolean;
|
||||
}>;
|
||||
|
||||
export const AppContainer = ({
|
||||
resizing,
|
||||
useNoisyBackground,
|
||||
useBlurBackground,
|
||||
children,
|
||||
@@ -39,7 +35,6 @@ export const AppContainer = ({
|
||||
'blur-background': blurBackground,
|
||||
})}
|
||||
data-noise-background={noisyBackground}
|
||||
data-is-resizing={resizing}
|
||||
data-blur-background={blurBackground}
|
||||
>
|
||||
{children}
|
||||
@@ -53,7 +48,6 @@ export const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<MainContainerProps>
|
||||
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
|
||||
const appSideBarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
return (
|
||||
@@ -63,7 +57,6 @@ export const MainContainer = forwardRef<
|
||||
data-is-desktop={BUILD_CONFIG.isElectron}
|
||||
data-transparent={false}
|
||||
data-client-border={appSettings.clientBorder}
|
||||
data-side-bar-open={appSideBarOpen}
|
||||
data-testid="main-container"
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -3,27 +3,25 @@ import {
|
||||
type InlineEditHandle,
|
||||
observeResize,
|
||||
} from '@affine/component';
|
||||
import { SharePageButton } from '@affine/core/components/affine/share-page-modal';
|
||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||
import { InfoButton } from '@affine/core/components/blocksuite/block-suite-header/info';
|
||||
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||
import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button';
|
||||
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affine/use-register-copy-link-commands';
|
||||
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { HeaderDivider } from '@affine/core/components/pure/header';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { SharePageButton } from '../../../../components/affine/share-page-modal';
|
||||
import { appSidebarFloatingAtom } from '../../../../components/app-sidebar';
|
||||
import { BlocksuiteHeaderTitle } from '../../../../components/blocksuite/block-suite-header/title/index';
|
||||
import { HeaderDivider } from '../../../../components/pure/header';
|
||||
import * as styles from './detail-page-header.css';
|
||||
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||
|
||||
@@ -35,15 +33,8 @@ const Header = forwardRef<
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
>(({ children, style, className }, ref) => {
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
return (
|
||||
<div
|
||||
data-testid="header"
|
||||
style={style}
|
||||
className={className}
|
||||
ref={ref}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
>
|
||||
<div data-testid="header" style={style} className={className} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { AppSidebarState } from '../providers/storage';
|
||||
|
||||
enum APP_SIDEBAR_STATE {
|
||||
OPEN = 'open',
|
||||
WIDTH = 'width',
|
||||
}
|
||||
|
||||
export class AppSidebar extends Entity {
|
||||
constructor(private readonly appSidebarState: AppSidebarState) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* whether the sidebar is open,
|
||||
* even if the sidebar is not open, hovering can show the floating sidebar
|
||||
*/
|
||||
open$ = LiveData.from(
|
||||
this.appSidebarState
|
||||
.watch<boolean>(APP_SIDEBAR_STATE.OPEN)
|
||||
.pipe(map(value => value ?? true)),
|
||||
true
|
||||
);
|
||||
|
||||
width$ = LiveData.from(
|
||||
this.appSidebarState
|
||||
.watch<number>(APP_SIDEBAR_STATE.WIDTH)
|
||||
.pipe(map(value => value ?? 248)),
|
||||
248
|
||||
);
|
||||
|
||||
/**
|
||||
* hovering can show the floating sidebar, without open it
|
||||
*/
|
||||
hovering$ = new LiveData<boolean>(false);
|
||||
|
||||
/**
|
||||
* small screen mode, will disable hover effect
|
||||
*/
|
||||
smallScreenMode$ = new LiveData<boolean>(false);
|
||||
resizing$ = new LiveData<boolean>(false);
|
||||
|
||||
getCachedAppSidebarOpenState = () => {
|
||||
return this.appSidebarState.get<boolean>(APP_SIDEBAR_STATE.OPEN);
|
||||
};
|
||||
|
||||
toggleSidebar = () => {
|
||||
this.setOpen(!this.open$.value);
|
||||
};
|
||||
|
||||
setOpen = (open: boolean) => {
|
||||
this.appSidebarState.set(APP_SIDEBAR_STATE.OPEN, open);
|
||||
return;
|
||||
};
|
||||
|
||||
setSmallScreenMode = (smallScreenMode: boolean) => {
|
||||
this.smallScreenMode$.next(smallScreenMode);
|
||||
};
|
||||
|
||||
setHovering = (hoverFloating: boolean) => {
|
||||
this.hovering$.next(hoverFloating);
|
||||
};
|
||||
|
||||
setResizing = (resizing: boolean) => {
|
||||
this.resizing$.next(resizing);
|
||||
};
|
||||
|
||||
setWidth = (width: number) => {
|
||||
this.appSidebarState.set(APP_SIDEBAR_STATE.WIDTH, width);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
type GlobalState,
|
||||
type Memento,
|
||||
wrapMemento,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import type { AppSidebarState } from '../providers/storage';
|
||||
|
||||
export class AppSidebarStateImpl implements AppSidebarState {
|
||||
wrapped: Memento;
|
||||
constructor(globalState: GlobalState) {
|
||||
this.wrapped = wrapMemento(globalState, `app-sidebar-state:`);
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
return this.wrapped.keys();
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.wrapped.get<T>(key);
|
||||
}
|
||||
|
||||
watch<T>(key: string) {
|
||||
return this.wrapped.watch<T>(key);
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
return this.wrapped.set<T>(key, value);
|
||||
}
|
||||
|
||||
del(key: string): void {
|
||||
return this.wrapped.del(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
return this.wrapped.clear();
|
||||
}
|
||||
}
|
||||
15
packages/frontend/core/src/modules/app-sidebar/index.ts
Normal file
15
packages/frontend/core/src/modules/app-sidebar/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type Framework, GlobalState } from '@toeverything/infra';
|
||||
|
||||
import { AppSidebar } from './entities/app-sidebar';
|
||||
import { AppSidebarStateImpl } from './impls/storage';
|
||||
import { AppSidebarState } from './providers/storage';
|
||||
import { AppSidebarService } from './services/app-sidebar';
|
||||
|
||||
export * from './services/app-sidebar';
|
||||
|
||||
export function configureAppSidebarModule(framework: Framework) {
|
||||
framework
|
||||
.service(AppSidebarService)
|
||||
.entity(AppSidebar, [AppSidebarState])
|
||||
.impl(AppSidebarState, AppSidebarStateImpl, [GlobalState]);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createIdentifier, type Memento } from '@toeverything/infra';
|
||||
|
||||
export interface AppSidebarState extends Memento {}
|
||||
|
||||
export const AppSidebarState =
|
||||
createIdentifier<AppSidebarState>('AppSidebarState');
|
||||
@@ -0,0 +1,7 @@
|
||||
import { GlobalState, Service } from '@toeverything/infra';
|
||||
|
||||
import { AppSidebar } from '../entities/app-sidebar';
|
||||
|
||||
export class AppSidebarService extends Service {
|
||||
sidebar = this.framework.createEntity(AppSidebar, [GlobalState]);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const floatingMaxWidth = 768;
|
||||
export const navWrapperStyle = style({
|
||||
@@ -10,16 +11,45 @@ export const navWrapperStyle = style({
|
||||
},
|
||||
selectors: {
|
||||
'&[data-has-border=true]': {
|
||||
borderRight: `0.5px solid ${cssVar('borderColor')}`,
|
||||
borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
},
|
||||
'&[data-is-floating="true"]': {
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
},
|
||||
'&[data-client-border="true"]': {
|
||||
paddingBottom: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const hoverNavWrapperStyle = style({
|
||||
selectors: {
|
||||
'&[data-is-floating="true"]': {
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
height: 'calc(100% - 60px)',
|
||||
marginTop: '52px',
|
||||
marginLeft: '4px',
|
||||
boxShadow: cssVar('--affine-popover-shadow'),
|
||||
borderRadius: '6px',
|
||||
},
|
||||
'&[data-is-floating="true"][data-is-electron="true"]': {
|
||||
height: '100%',
|
||||
marginTop: '-4px',
|
||||
},
|
||||
'&[data-is-floating="true"][data-client-border="true"]': {
|
||||
backgroundColor: cssVarV2('layer/background/overlayPanel'),
|
||||
},
|
||||
'&[data-is-floating="true"][data-client-border="true"]::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: `var(--affine-noise-opacity, 0)`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '50px',
|
||||
// TODO(@Peng): figure out how to use vanilla-extract webpack plugin to inject img url
|
||||
backgroundImage: `var(--noise-background)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const navHeaderButton = style({
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -62,7 +92,7 @@ export const sidebarFloatMaskStyle = style({
|
||||
left: 0,
|
||||
right: '100%',
|
||||
bottom: 0,
|
||||
background: cssVar('backgroundModalColor'),
|
||||
background: cssVarV2('layer/background/modal'),
|
||||
selectors: {
|
||||
'&[data-open="true"][data-is-floating="true"]': {
|
||||
opacity: 1,
|
||||
@@ -2,29 +2,29 @@ import { Skeleton } from '@affine/component';
|
||||
import { ResizePanel } from '@affine/component/resize-panel';
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useServiceOptional, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { WorkspaceNavigator } from '@affine/core/components/workspace-selector';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { WorkspaceNavigator } from '../workspace-selector';
|
||||
import { AppSidebarService } from '../services/app-sidebar';
|
||||
import * as styles from './fallback.css';
|
||||
import {
|
||||
floatingMaxWidth,
|
||||
hoverNavWrapperStyle,
|
||||
navBodyStyle,
|
||||
navHeaderStyle,
|
||||
navStyle,
|
||||
navWrapperStyle,
|
||||
sidebarFloatMaskStyle,
|
||||
} from './index.css';
|
||||
import {
|
||||
APP_SIDEBAR_OPEN,
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
appSidebarResizingAtom,
|
||||
appSidebarWidthAtom,
|
||||
} from './index.jotai';
|
||||
import { SidebarHeader } from './sidebar-header';
|
||||
|
||||
export type History = {
|
||||
@@ -34,16 +34,56 @@ export type History = {
|
||||
|
||||
const MAX_WIDTH = 480;
|
||||
const MIN_WIDTH = 248;
|
||||
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
||||
|
||||
export function AppSidebar({ children }: PropsWithChildren) {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const clientBorder = appSettings.clientBorder;
|
||||
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
const [width, setWidth] = useAtom(appSidebarWidthAtom);
|
||||
const [floating, setFloating] = useAtom(appSidebarFloatingAtom);
|
||||
const [resizing, setResizing] = useAtom(appSidebarResizingAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
const width = useLiveData(appSidebarService.width$);
|
||||
const smallScreenMode = useLiveData(appSidebarService.smallScreenMode$);
|
||||
const hovering = useLiveData(appSidebarService.hovering$) && open !== true;
|
||||
const resizing = useLiveData(appSidebarService.resizing$);
|
||||
const [deferredHovering, setDeferredHovering] = useState(false);
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// if open, we don't need to show the floating sidebar
|
||||
setDeferredHovering(false);
|
||||
return;
|
||||
}
|
||||
if (hovering) {
|
||||
// if hovering is true, we make a little delay here.
|
||||
// this allow the sidebar close animation to complete.
|
||||
const timeout = setTimeout(() => {
|
||||
setDeferredHovering(hovering);
|
||||
}, 150);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
} else {
|
||||
// if hovering is false, we set the deferred value after 1000ms
|
||||
const timeout = setTimeout(() => {
|
||||
setDeferredHovering(hovering);
|
||||
}, 1000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [hovering, open]);
|
||||
|
||||
const sidebarState = smallScreenMode
|
||||
? open
|
||||
? 'floating-with-mask'
|
||||
: 'close'
|
||||
: open
|
||||
? 'open'
|
||||
: deferredHovering
|
||||
? 'floating'
|
||||
: 'close';
|
||||
|
||||
useEffect(() => {
|
||||
// do not float app sidebar on desktop
|
||||
@@ -55,19 +95,8 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
||||
const isFloatingMaxWidth = window.matchMedia(
|
||||
`(max-width: ${floatingMaxWidth}px)`
|
||||
).matches;
|
||||
const isOverflowWidth = window.matchMedia(
|
||||
`(max-width: ${width / 0.4}px)`
|
||||
).matches;
|
||||
const isFloating = isFloatingMaxWidth || isOverflowWidth;
|
||||
if (
|
||||
open === undefined &&
|
||||
localStorage.getItem(APP_SIDEBAR_OPEN) === null
|
||||
) {
|
||||
// give the initial value,
|
||||
// so that the sidebar can be closed on mobile by default
|
||||
setOpen(!isFloating);
|
||||
}
|
||||
setFloating(isFloating);
|
||||
const isFloating = isFloatingMaxWidth;
|
||||
appSidebarService.setSmallScreenMode(isFloating);
|
||||
}
|
||||
|
||||
const dOnResize = debounce(onResize, 50);
|
||||
@@ -75,36 +104,77 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
||||
return () => {
|
||||
window.removeEventListener('resize', dOnResize);
|
||||
};
|
||||
}, [open, setFloating, setOpen, width]);
|
||||
}, [appSidebarService]);
|
||||
|
||||
const hasRightBorder = !BUILD_CONFIG.isElectron && !clientBorder;
|
||||
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
appSidebarService.setOpen(open);
|
||||
},
|
||||
[appSidebarService]
|
||||
);
|
||||
|
||||
const handleResizing = useCallback(
|
||||
(resizing: boolean) => {
|
||||
appSidebarService.setResizing(resizing);
|
||||
},
|
||||
[appSidebarService]
|
||||
);
|
||||
|
||||
const handleWidthChange = useCallback(
|
||||
(width: number) => {
|
||||
appSidebarService.setWidth(width);
|
||||
},
|
||||
[appSidebarService]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
appSidebarService.setOpen(false);
|
||||
}, [appSidebarService]);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
appSidebarService.setHovering(true);
|
||||
}, [appSidebarService]);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
appSidebarService.setHovering(false);
|
||||
}, [appSidebarService]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizePanel
|
||||
floating={floating}
|
||||
open={open}
|
||||
floating={
|
||||
sidebarState === 'floating' || sidebarState === 'floating-with-mask'
|
||||
}
|
||||
open={sidebarState !== 'close'}
|
||||
resizing={resizing}
|
||||
maxWidth={MAX_WIDTH}
|
||||
minWidth={MIN_WIDTH}
|
||||
width={width}
|
||||
resizeHandlePos="right"
|
||||
onOpen={setOpen}
|
||||
onResizing={setResizing}
|
||||
onWidthChange={setWidth}
|
||||
className={navWrapperStyle}
|
||||
onOpen={handleOpenChange}
|
||||
onResizing={handleResizing}
|
||||
onWidthChange={handleWidthChange}
|
||||
className={clsx(navWrapperStyle, {
|
||||
[hoverNavWrapperStyle]: sidebarState === 'floating',
|
||||
})}
|
||||
resizeHandleOffset={0}
|
||||
resizeHandleVerticalPadding={clientBorder ? 16 : 0}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
data-transparent
|
||||
data-open={open}
|
||||
data-open={sidebarState !== 'close'}
|
||||
data-has-border={hasRightBorder}
|
||||
data-testid="app-sidebar-wrapper"
|
||||
data-is-macos-electron={isMacosDesktop}
|
||||
data-client-border={clientBorder}
|
||||
data-is-electron={BUILD_CONFIG.isElectron}
|
||||
>
|
||||
<nav className={navStyle} data-testid="app-sidebar">
|
||||
{!BUILD_CONFIG.isElectron && <SidebarHeader />}
|
||||
{!BUILD_CONFIG.isElectron && sidebarState !== 'floating' && (
|
||||
<SidebarHeader />
|
||||
)}
|
||||
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
||||
{children}
|
||||
</div>
|
||||
@@ -113,9 +183,9 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
||||
<div
|
||||
data-testid="app-sidebar-float-mask"
|
||||
data-open={open}
|
||||
data-is-floating={floating}
|
||||
data-is-floating={sidebarState === 'floating-with-mask'}
|
||||
className={sidebarFloatMaskStyle}
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -207,7 +277,8 @@ const FallbackBody = () => {
|
||||
};
|
||||
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
const width = useAtomValue(appSidebarWidthAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const width = useLiveData(appSidebarService.width$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const clientBorder = appSettings.clientBorder;
|
||||
|
||||
@@ -235,7 +306,8 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
||||
* NOTE(@forehalo): this is a copy of [AppSidebarFallback] without [WorkspaceNavigator] which will introduce a lot useless dependencies for shell(tab bar)
|
||||
*/
|
||||
export const ShellAppSidebarFallback = () => {
|
||||
const width = useAtomValue(appSidebarWidthAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const width = useLiveData(appSidebarService.width$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const clientBorder = appSettings.clientBorder;
|
||||
|
||||
@@ -268,4 +340,3 @@ export * from './menu-item';
|
||||
export * from './quick-search-input';
|
||||
export * from './sidebar-containers';
|
||||
export * from './sidebar-header';
|
||||
export { appSidebarFloatingAtom, appSidebarOpenAtom, appSidebarResizingAtom };
|
||||
@@ -3,7 +3,7 @@ import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
import { MenuLinkItem } from '.';
|
||||
import { MenuLinkItem } from './index';
|
||||
|
||||
const RawLink = ({
|
||||
children,
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { AppSidebarService } from '../../services/app-sidebar';
|
||||
import { navHeaderStyle } from '../index.css';
|
||||
import { appSidebarOpenAtom } from '../index.jotai';
|
||||
import { SidebarSwitch } from './sidebar-switch';
|
||||
|
||||
export const SidebarHeader = () => {
|
||||
const open = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
|
||||
return (
|
||||
<div className={navHeaderStyle} data-open={open}>
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SidebarIcon } from '@blocksuite/icons/rc';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { appSidebarOpenAtom } from '../index.jotai';
|
||||
import { AppSidebarService } from '../../services/app-sidebar';
|
||||
import * as styles from './sidebar-switch.css';
|
||||
|
||||
export const SidebarSwitch = ({
|
||||
@@ -13,7 +14,22 @@ export const SidebarSwitch = ({
|
||||
show: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const open = useLiveData(appSidebarService.open$);
|
||||
const switchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
appSidebarService.setHovering(true);
|
||||
}, [appSidebarService]);
|
||||
|
||||
const handleClickSwitch = useCallback(() => {
|
||||
appSidebarService.toggleSidebar();
|
||||
}, [appSidebarService]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
appSidebarService.setHovering(false);
|
||||
}, [appSidebarService]);
|
||||
|
||||
const t = useI18n();
|
||||
const tooltipContent = open
|
||||
? t['com.affine.sidebarSwitch.collapse']()
|
||||
@@ -21,9 +37,12 @@ export const SidebarSwitch = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={switchRef}
|
||||
data-show={show}
|
||||
className={styles.sidebarSwitchClip}
|
||||
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tooltipContent}
|
||||
@@ -34,7 +53,7 @@ export const SidebarSwitch = ({
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
onClick={() => setOpen(open => !open)}
|
||||
onClick={handleClickSwitch}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</IconButton>
|
||||
@@ -7,11 +7,6 @@ import {
|
||||
useDraggable,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
appSidebarOpenAtom,
|
||||
appSidebarResizingAtom,
|
||||
} from '@affine/core/components/app-sidebar';
|
||||
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
@@ -25,7 +20,6 @@ import {
|
||||
useServiceOptional,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { partition } from 'lodash-es';
|
||||
import {
|
||||
Fragment,
|
||||
@@ -35,6 +29,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { AppSidebarService } from '../../app-sidebar';
|
||||
import { iconNameToIcon } from '../../workbench/constants';
|
||||
import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer';
|
||||
import {
|
||||
@@ -298,9 +293,12 @@ export const AppTabsHeader = ({
|
||||
left?: ReactNode;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const sidebarWidth = useAtomValue(appSidebarWidthAtom);
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
const sidebarResizing = useAtomValue(appSidebarResizingAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const sidebarWidth = useLiveData(appSidebarService.width$);
|
||||
const sidebarOpen = useLiveData(appSidebarService.open$);
|
||||
const sidebarResizing = useLiveData(appSidebarService.resizing$);
|
||||
const hoverFloating = useLiveData(appSidebarService.hoverFloating$);
|
||||
|
||||
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
||||
const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows;
|
||||
const fullScreen = useIsFullScreen();
|
||||
@@ -417,9 +415,12 @@ export const AppTabsHeader = ({
|
||||
style={{
|
||||
transition: sidebarResizing ? 'none' : undefined,
|
||||
paddingLeft: 12 + trafficLightOffset,
|
||||
width: sidebarOpen ? sidebarWidth : 120 + trafficLightOffset,
|
||||
width:
|
||||
sidebarOpen && !hoverFloating
|
||||
? sidebarWidth
|
||||
: 120 + trafficLightOffset,
|
||||
// minus 16 to account for the padding on the right side of the header (for box shadow)
|
||||
marginRight: sidebarOpen ? -16 : 0,
|
||||
marginRight: sidebarOpen && !hoverFloating ? -16 : 0,
|
||||
}}
|
||||
className={styles.headerLeft}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CategoryDivider } from '@affine/core/components/app-sidebar';
|
||||
import { CategoryDivider } from '@affine/core/modules/app-sidebar/views';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
@@ -21,10 +21,10 @@ import {
|
||||
MoreHorizontalIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import type { To } from 'history';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import {
|
||||
Fragment,
|
||||
type RefAttributes,
|
||||
@@ -117,10 +117,13 @@ export const ExplorerTreeNode = ({
|
||||
// If no onClick or to is provided, clicking on the node will toggle the collapse state
|
||||
const clickForCollapse = !onClick && !to && !disabled;
|
||||
const [childCount, setChildCount] = useState(0);
|
||||
const sidebarWidth = useAtomValue(appSidebarWidthAtom);
|
||||
const [renaming, setRenaming] = useState(defaultRenaming);
|
||||
const [lastInGroup, setLastInGroup] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const sidebarWidth = useLiveData(appSidebarService.width$);
|
||||
|
||||
const { emoji, name } = useMemo(() => {
|
||||
if (!extractEmojiAsIcon || !rawName) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { configureQuotaModule } from '@affine/core/modules/quota';
|
||||
import { configureInfraModules, type Framework } from '@toeverything/infra';
|
||||
|
||||
import { configureAppSidebarModule } from './app-sidebar';
|
||||
import { configureCloudModule } from './cloud';
|
||||
import { configureCollectionModule } from './collection';
|
||||
import { configureCreateWorkspaceModule } from './create-workspace';
|
||||
@@ -57,4 +58,5 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureCreateWorkspaceModule(framework);
|
||||
configureUserspaceModule(framework);
|
||||
configureDocInfoModule(framework);
|
||||
configureAppSidebarModule(framework);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
||||
import { SidebarSwitch } from '../../../components/app-sidebar/sidebar-header/sidebar-switch';
|
||||
import { AppSidebarService } from '../../app-sidebar';
|
||||
import { SidebarSwitch } from '../../app-sidebar/views';
|
||||
import { ViewService } from '../services/view';
|
||||
import { WorkbenchService } from '../services/workbench';
|
||||
import * as styles from './route-container.css';
|
||||
@@ -43,7 +42,11 @@ const ToggleButton = ({
|
||||
|
||||
export const RouteContainer = () => {
|
||||
const viewPosition = useViewPosition();
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||
const leftSidebarOpen = useLiveData(appSidebarService.open$);
|
||||
const leftSidebarHoverFloating = useLiveData(
|
||||
appSidebarService.hoverFloating$
|
||||
);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const view = useService(ViewService).view;
|
||||
const sidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||
@@ -56,7 +59,8 @@ export const RouteContainer = () => {
|
||||
<div className={styles.header}>
|
||||
{!BUILD_CONFIG.isElectron && viewPosition.isFirst && (
|
||||
<SidebarSwitch
|
||||
show={!leftSidebarOpen}
|
||||
show={leftSidebarHoverFloating || !leftSidebarOpen}
|
||||
enableOpenHoverSidebar
|
||||
className={styles.leftSidebarButton}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const SidebarContainer = ({
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.sidebarContainerInner, className)} {...props}>
|
||||
<Header floating={false} onToggle={handleToggleOpen}>
|
||||
<Header onToggle={handleToggleOpen}>
|
||||
<SidebarHeaderSwitcher />
|
||||
</Header>
|
||||
{sidebarTabs.length > 0 ? (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
import * as styles from './sidebar-header.css';
|
||||
|
||||
export type HeaderProps = {
|
||||
floating: boolean;
|
||||
onToggle?: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
@@ -13,20 +12,13 @@ function Container({
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
floating,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
floating?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-testid="header"
|
||||
style={style}
|
||||
className={className}
|
||||
data-sidebar-floating={floating}
|
||||
>
|
||||
<div data-testid="header" style={style} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -40,9 +32,9 @@ const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = ({ floating, children, onToggle }: HeaderProps) => {
|
||||
export const Header = ({ children, onToggle }: HeaderProps) => {
|
||||
return (
|
||||
<Container className={styles.header} floating={floating}>
|
||||
<Container className={styles.header}>
|
||||
{children}
|
||||
{!BUILD_CONFIG.isElectron && (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,9 @@ test('Collapse Sidebar', async ({ page }) => {
|
||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||
.click();
|
||||
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||
await sliderBarArea.hover();
|
||||
await page.mouse.move(300, 300);
|
||||
await page.waitForTimeout(5000);
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
@@ -20,6 +23,9 @@ test('Expand Sidebar', async ({ page }) => {
|
||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||
.click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await sliderBarArea.hover();
|
||||
await page.mouse.move(300, 300);
|
||||
await page.waitForTimeout(5000);
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
|
||||
await page
|
||||
|
||||
Reference in New Issue
Block a user