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:
pengx17
2024-09-06 09:25:20 +00:00
parent 16bb00ed78
commit d089470bbf
17 changed files with 223 additions and 59 deletions

View File

@@ -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>
);
};

View File

@@ -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)',
});

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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%',
});

View File

@@ -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 => {

View File

@@ -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>
);
});

View File

@@ -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>
);
};

View File

@@ -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>
);
}