mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(electron): electron shell skeleton (#8127)
fix AF-1331
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/e09203aa-f143-42f8-bd39-e5078d07ada2.mp4">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/e09203aa-f143-42f8-bd39-e5078d07ada2.mp4">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/e09203aa-f143-42f8-bd39-e5078d07ada2.mp4">1.mp4</video>
missing
- per split view skeleton
- per route skeleton
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { AppSidebarFallback } from '../app-sidebar';
|
||||
import type { WorkspaceRootProps } from '../workspace';
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
MainContainer,
|
||||
MainContainerFallback,
|
||||
} from '../workspace';
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
@@ -24,11 +24,16 @@ export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AppFallback = (): ReactElement => {
|
||||
export const AppFallback = ({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
}>): ReactElement => {
|
||||
return (
|
||||
<AppContainer>
|
||||
<AppContainer className={className}>
|
||||
<AppSidebarFallback />
|
||||
<MainContainer />
|
||||
<MainContainerFallback>{children}</MainContainerFallback>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const fallbackStyle = style({
|
||||
margin: '4px 16px',
|
||||
|
||||
export const fallback = style({
|
||||
padding: '4px 20px',
|
||||
height: '100%',
|
||||
overflow: 'clip',
|
||||
});
|
||||
export const fallbackHeaderStyle = style({
|
||||
|
||||
export const fallbackHeader = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
gap: '8px',
|
||||
overflow: 'hidden',
|
||||
height: '52px',
|
||||
});
|
||||
|
||||
export const spacer = style({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const fallbackBody = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '42px',
|
||||
marginTop: '42px',
|
||||
});
|
||||
|
||||
export const fallbackGroupItems = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
export const fallbackItemHeader = style({
|
||||
transform: 'translateX(-10px)',
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { ResizePanel } from '@affine/component/resize-panel';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { NavigateContext } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { useServiceOptional, WorkspaceService } from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
|
||||
import { WorkspaceNavigator } from '../workspace-selector';
|
||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||
import * as styles from './fallback.css';
|
||||
import {
|
||||
floatingMaxWidth,
|
||||
navBodyStyle,
|
||||
@@ -25,11 +27,6 @@ import {
|
||||
} from './index.jotai';
|
||||
import { SidebarHeader } from './sidebar-header';
|
||||
|
||||
export type AppSidebarProps = PropsWithChildren<{
|
||||
clientBorder?: boolean;
|
||||
translucentUI?: boolean;
|
||||
}>;
|
||||
|
||||
export type History = {
|
||||
stack: string[];
|
||||
current: number;
|
||||
@@ -38,10 +35,11 @@ export type History = {
|
||||
const MAX_WIDTH = 480;
|
||||
const MIN_WIDTH = 248;
|
||||
|
||||
export function AppSidebar({
|
||||
children,
|
||||
clientBorder,
|
||||
}: AppSidebarProps): ReactElement {
|
||||
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);
|
||||
@@ -122,35 +120,96 @@ export function AppSidebar({
|
||||
);
|
||||
}
|
||||
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
const width = useAtomValue(appSidebarWidthAtom);
|
||||
const FallbackHeader = () => {
|
||||
// if navigate is not defined, it is rendered outside of router
|
||||
// WorkspaceNavigator requires navigate context
|
||||
// todo: refactor
|
||||
const navigate = useContext(NavigateContext);
|
||||
|
||||
const currentWorkspace = useServiceOptional(WorkspaceService);
|
||||
return (
|
||||
<div className={styles.fallbackHeader}>
|
||||
{!currentWorkspace && navigate ? (
|
||||
<WorkspaceNavigator
|
||||
showSettingsButton
|
||||
showSyncStatus
|
||||
showEnableCloudButton
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton variant="rectangular" width={32} height={32} />
|
||||
<Skeleton variant="rectangular" width={150} height={32} flex={1} />
|
||||
<Skeleton variant="circular" width={25} height={25} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const randomWidth = () => {
|
||||
return Math.floor(Math.random() * 200) + 100;
|
||||
};
|
||||
|
||||
const RandomBar = ({ className }: { className?: string }) => {
|
||||
const width = useMemo(() => randomWidth(), []);
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={width}
|
||||
height={16}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RandomBars = ({ count, header }: { count: number; header?: boolean }) => {
|
||||
return (
|
||||
<div className={styles.fallbackGroupItems}>
|
||||
{header ? (
|
||||
<Skeleton
|
||||
className={styles.fallbackItemHeader}
|
||||
variant="rectangular"
|
||||
width={50}
|
||||
height={16}
|
||||
/>
|
||||
) : null}
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<RandomBar key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FallbackBody = () => {
|
||||
return (
|
||||
<div className={styles.fallbackBody}>
|
||||
<RandomBars count={3} />
|
||||
<RandomBars count={4} header />
|
||||
<RandomBars count={4} header />
|
||||
<RandomBars count={3} header />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
const width = useAtomValue(appSidebarWidthAtom);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const clientBorder = appSettings.clientBorder;
|
||||
const hasRightBorder = !environment.isElectron && !clientBorder;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ width }}
|
||||
className={navWrapperStyle}
|
||||
data-has-border
|
||||
data-has-border={hasRightBorder}
|
||||
data-open="true"
|
||||
>
|
||||
<nav className={navStyle}>
|
||||
<div className={navHeaderStyle} data-open="true" />
|
||||
{!environment.isElectron ? <div className={navHeaderStyle} /> : null}
|
||||
<div className={navBodyStyle}>
|
||||
<div className={fallbackStyle}>
|
||||
<div className={fallbackHeaderStyle}>
|
||||
{currentWorkspace ? (
|
||||
<WorkspaceNavigator
|
||||
showSettingsButton
|
||||
showSyncStatus
|
||||
showEnableCloudButton
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton variant="circular" width={40} height={40} />
|
||||
<Skeleton variant="rectangular" width={150} height={40} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.fallback}>
|
||||
<FallbackHeader />
|
||||
<FallbackBody />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -29,7 +29,6 @@ import { useSetAtom } from 'jotai';
|
||||
import type { MouseEvent, ReactElement } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { WorkbenchService } from '../../modules/workbench';
|
||||
import {
|
||||
AddPageButton,
|
||||
@@ -84,7 +83,6 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
CMDKQuickSearchService,
|
||||
});
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const t = useI18n();
|
||||
const workbench = workbenchService.workbench;
|
||||
@@ -141,10 +139,7 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
return (
|
||||
<AppSidebar
|
||||
clientBorder={appSettings.clientBorder}
|
||||
translucentUI={appSettings.enableBlurBackground}
|
||||
>
|
||||
<AppSidebar>
|
||||
<SidebarContainer>
|
||||
<div className={workspaceAndUserWrapper}>
|
||||
<div className={workspaceWrapper}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cssVar, lightCssVariables } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const appStyle = style({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
@@ -98,3 +99,11 @@ export const toolStyle = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const fallbackRootStyle = style({
|
||||
paddingTop: 52,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
|
||||
export type WorkspaceRootProps = PropsWithChildren<{
|
||||
resizing?: boolean;
|
||||
className?: string;
|
||||
useNoisyBackground?: boolean;
|
||||
useBlurBackground?: boolean;
|
||||
}>;
|
||||
@@ -24,6 +25,7 @@ export const AppContainer = ({
|
||||
useNoisyBackground,
|
||||
useBlurBackground,
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: WorkspaceRootProps) => {
|
||||
const noisyBackground = useNoisyBackground && environment.isElectron;
|
||||
@@ -31,7 +33,7 @@ export const AppContainer = ({
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={clsx(appStyle, {
|
||||
className={clsx(appStyle, className, {
|
||||
'noisy-background': noisyBackground,
|
||||
'blur-background': blurBackground,
|
||||
})}
|
||||
@@ -71,6 +73,11 @@ export const MainContainer = forwardRef<
|
||||
|
||||
MainContainer.displayName = 'MainContainer';
|
||||
|
||||
export const MainContainerFallback = ({ children }: PropsWithChildren) => {
|
||||
// todo: default app fallback?
|
||||
return <MainContainer>{children}</MainContainer>;
|
||||
};
|
||||
|
||||
export const ToolContainer = (
|
||||
props: PropsWithChildren<{ className?: string }>
|
||||
): ReactElement => {
|
||||
|
||||
@@ -39,6 +39,17 @@ export interface SplitViewPanelProps
|
||||
>;
|
||||
}
|
||||
|
||||
export const SplitViewPanelContainer = ({
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={styles.splitViewPanel} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SplitViewPanel = memo(function SplitViewPanel({
|
||||
children,
|
||||
view,
|
||||
@@ -85,9 +96,8 @@ export const SplitViewPanel = memo(function SplitViewPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
<SplitViewPanelContainer
|
||||
style={style}
|
||||
className={styles.splitViewPanel}
|
||||
data-is-dragging={isDragging}
|
||||
data-is-active={isActive && views.length > 1}
|
||||
data-is-last={isLast}
|
||||
@@ -110,7 +120,7 @@ export const SplitViewPanel = memo(function SplitViewPanel({
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</SplitViewPanelContainer>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes, RefObject } from 'react';
|
||||
import type { HTMLAttributes, PropsWithChildren, RefObject } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { View } from '../../entities/view';
|
||||
import { WorkbenchService } from '../../services/workbench';
|
||||
import { SplitViewPanel } from './panel';
|
||||
import { SplitViewPanel, SplitViewPanelContainer } from './panel';
|
||||
import { ResizeHandle } from './resize-handle';
|
||||
import * as styles from './split-view.css';
|
||||
|
||||
@@ -141,3 +141,20 @@ export const SplitView = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SplitViewFallback = ({
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{ className?: string }>) => {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.splitViewRoot, className)}
|
||||
data-client-border={appSettings.clientBorder}
|
||||
>
|
||||
{/* todo: support multiple split views */}
|
||||
<SplitViewPanelContainer>{children}</SplitViewPanelContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ export const Component = (): ReactElement => {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
if (!meta) {
|
||||
return <AppFallback key="workspaceLoading" />;
|
||||
return <AppFallback />;
|
||||
}
|
||||
|
||||
return <WorkspacePage meta={meta} />;
|
||||
@@ -204,7 +204,7 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||
if (!isRootDocReady) {
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppFallback key="workspaceLoading" />
|
||||
<AppFallback />
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user