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
@@ -29,6 +29,7 @@ const waveKeyframes = keyframes({
export const root = style({ export const root = style({
display: 'block', display: 'block',
width: '100%', width: '100%',
maxWidth: '100%',
height: defaultHeight, height: defaultHeight,
flexShrink: 0, flexShrink: 0,
/** /**
@@ -51,7 +52,7 @@ export const variant = {
borderRadius: '50%', borderRadius: '50%',
}), }),
rectangular: style({ rectangular: style({
borderRadius: '0px', borderRadius: '4px',
}), }),
rounded: style({ rounded: style({
borderRadius: '8px', borderRadius: '8px',
@@ -16,6 +16,7 @@ export const Skeleton = ({
variant = 'text', variant = 'text',
children, children,
flex,
width: _width, width: _width,
height: _height, height: _height,
style: _style, style: _style,
@@ -29,6 +30,7 @@ export const Skeleton = ({
const style = { const style = {
width, width,
height, height,
flex,
..._style, ..._style,
}; };
@@ -28,6 +28,11 @@ export interface SkeletonProps
* Number values are treated as pixels. * Number values are treated as pixels.
*/ */
height?: number | string; height?: number | string;
/**
* Flex of the skeleton.
*/
flex?: number | string;
} }
export type PickStringFromUnion<T> = T extends string ? T : never; export type PickStringFromUnion<T> = T extends string ? T : never;
@@ -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 { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { AppSidebarFallback } from '../app-sidebar'; import { AppSidebarFallback } from '../app-sidebar';
import type { WorkspaceRootProps } from '../workspace'; import type { WorkspaceRootProps } from '../workspace';
import { import {
AppContainer as AppContainerWithoutSettings, AppContainer as AppContainerWithoutSettings,
MainContainer, MainContainerFallback,
} from '../workspace'; } from '../workspace';
export const AppContainer = (props: WorkspaceRootProps) => { 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 ( return (
<AppContainer> <AppContainer className={className}>
<AppSidebarFallback /> <AppSidebarFallback />
<MainContainer /> <MainContainerFallback>{children}</MainContainerFallback>
</AppContainer> </AppContainer>
); );
}; };
@@ -1,12 +1,38 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const fallbackStyle = style({
margin: '4px 16px', export const fallback = style({
padding: '4px 20px',
height: '100%', height: '100%',
overflow: 'clip',
}); });
export const fallbackHeaderStyle = style({
export const fallbackHeader = style({
width: '100%', width: '100%',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
gap: '8px', 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 { Skeleton } from '@affine/component';
import { ResizePanel } from '@affine/component/resize-panel'; 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 { useServiceOptional, WorkspaceService } from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import { useEffect } from 'react'; import { useContext, useEffect, useMemo } from 'react';
import { WorkspaceNavigator } from '../workspace-selector'; import { WorkspaceNavigator } from '../workspace-selector';
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css'; import * as styles from './fallback.css';
import { import {
floatingMaxWidth, floatingMaxWidth,
navBodyStyle, navBodyStyle,
@@ -25,11 +27,6 @@ import {
} from './index.jotai'; } from './index.jotai';
import { SidebarHeader } from './sidebar-header'; import { SidebarHeader } from './sidebar-header';
export type AppSidebarProps = PropsWithChildren<{
clientBorder?: boolean;
translucentUI?: boolean;
}>;
export type History = { export type History = {
stack: string[]; stack: string[];
current: number; current: number;
@@ -38,10 +35,11 @@ export type History = {
const MAX_WIDTH = 480; const MAX_WIDTH = 480;
const MIN_WIDTH = 248; const MIN_WIDTH = 248;
export function AppSidebar({ export function AppSidebar({ children }: PropsWithChildren) {
children, const { appSettings } = useAppSettingHelper();
clientBorder,
}: AppSidebarProps): ReactElement { const clientBorder = appSettings.clientBorder;
const [open, setOpen] = useAtom(appSidebarOpenAtom); const [open, setOpen] = useAtom(appSidebarOpenAtom);
const [width, setWidth] = useAtom(appSidebarWidthAtom); const [width, setWidth] = useAtom(appSidebarWidthAtom);
const [floating, setFloating] = useAtom(appSidebarFloatingAtom); const [floating, setFloating] = useAtom(appSidebarFloatingAtom);
@@ -122,35 +120,96 @@ export function AppSidebar({
); );
} }
export const AppSidebarFallback = (): ReactElement | null => { const FallbackHeader = () => {
const width = useAtomValue(appSidebarWidthAtom); // 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); 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 ( return (
<div <div
style={{ width }} style={{ width }}
className={navWrapperStyle} className={navWrapperStyle}
data-has-border data-has-border={hasRightBorder}
data-open="true" data-open="true"
> >
<nav className={navStyle}> <nav className={navStyle}>
<div className={navHeaderStyle} data-open="true" /> {!environment.isElectron ? <div className={navHeaderStyle} /> : null}
<div className={navBodyStyle}> <div className={navBodyStyle}>
<div className={fallbackStyle}> <div className={styles.fallback}>
<div className={fallbackHeaderStyle}> <FallbackHeader />
{currentWorkspace ? ( <FallbackBody />
<WorkspaceNavigator
showSettingsButton
showSyncStatus
showEnableCloudButton
/>
) : (
<>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="rectangular" width={150} height={40} />
</>
)}
</div>
</div> </div>
</div> </div>
</nav> </nav>
@@ -29,7 +29,6 @@ import { useSetAtom } from 'jotai';
import type { MouseEvent, ReactElement } from 'react'; import type { MouseEvent, ReactElement } from 'react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { WorkbenchService } from '../../modules/workbench'; import { WorkbenchService } from '../../modules/workbench';
import { import {
AddPageButton, AddPageButton,
@@ -84,7 +83,6 @@ export const RootAppSidebar = (): ReactElement => {
CMDKQuickSearchService, CMDKQuickSearchService,
}); });
const currentWorkspace = workspaceService.workspace; const currentWorkspace = workspaceService.workspace;
const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection; const docCollection = currentWorkspace.docCollection;
const t = useI18n(); const t = useI18n();
const workbench = workbenchService.workbench; const workbench = workbenchService.workbench;
@@ -141,10 +139,7 @@ export const RootAppSidebar = (): ReactElement => {
}, [setOpenSettingModalAtom]); }, [setOpenSettingModalAtom]);
return ( return (
<AppSidebar <AppSidebar>
clientBorder={appSettings.clientBorder}
translucentUI={appSettings.enableBlurBackground}
>
<SidebarContainer> <SidebarContainer>
<div className={workspaceAndUserWrapper}> <div className={workspaceAndUserWrapper}>
<div className={workspaceWrapper}> <div className={workspaceWrapper}>
@@ -1,5 +1,6 @@
import { cssVar, lightCssVariables } from '@toeverything/theme'; import { cssVar, lightCssVariables } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const appStyle = style({ export const appStyle = style({
width: '100%', width: '100%',
position: 'relative', 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<{ export type WorkspaceRootProps = PropsWithChildren<{
resizing?: boolean; resizing?: boolean;
className?: string;
useNoisyBackground?: boolean; useNoisyBackground?: boolean;
useBlurBackground?: boolean; useBlurBackground?: boolean;
}>; }>;
@@ -24,6 +25,7 @@ export const AppContainer = ({
useNoisyBackground, useNoisyBackground,
useBlurBackground, useBlurBackground,
children, children,
className,
...rest ...rest
}: WorkspaceRootProps) => { }: WorkspaceRootProps) => {
const noisyBackground = useNoisyBackground && environment.isElectron; const noisyBackground = useNoisyBackground && environment.isElectron;
@@ -31,7 +33,7 @@ export const AppContainer = ({
return ( return (
<div <div
{...rest} {...rest}
className={clsx(appStyle, { className={clsx(appStyle, className, {
'noisy-background': noisyBackground, 'noisy-background': noisyBackground,
'blur-background': blurBackground, 'blur-background': blurBackground,
})} })}
@@ -71,6 +73,11 @@ export const MainContainer = forwardRef<
MainContainer.displayName = 'MainContainer'; MainContainer.displayName = 'MainContainer';
export const MainContainerFallback = ({ children }: PropsWithChildren) => {
// todo: default app fallback?
return <MainContainer>{children}</MainContainer>;
};
export const ToolContainer = ( export const ToolContainer = (
props: PropsWithChildren<{ className?: string }> props: PropsWithChildren<{ className?: string }>
): ReactElement => { ): 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({ export const SplitViewPanel = memo(function SplitViewPanel({
children, children,
view, view,
@@ -85,9 +96,8 @@ export const SplitViewPanel = memo(function SplitViewPanel({
); );
return ( return (
<div <SplitViewPanelContainer
style={style} style={style}
className={styles.splitViewPanel}
data-is-dragging={isDragging} data-is-dragging={isDragging}
data-is-active={isActive && views.length > 1} data-is-active={isActive && views.length > 1}
data-is-last={isLast} data-is-last={isLast}
@@ -110,7 +120,7 @@ export const SplitViewPanel = memo(function SplitViewPanel({
) : null} ) : null}
</div> </div>
{children} {children}
</div> </SplitViewPanelContainer>
); );
}); });
@@ -13,13 +13,13 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import clsx from 'clsx'; 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 { useCallback, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { View } from '../../entities/view'; import type { View } from '../../entities/view';
import { WorkbenchService } from '../../services/workbench'; import { WorkbenchService } from '../../services/workbench';
import { SplitViewPanel } from './panel'; import { SplitViewPanel, SplitViewPanelContainer } from './panel';
import { ResizeHandle } from './resize-handle'; import { ResizeHandle } from './resize-handle';
import * as styles from './split-view.css'; import * as styles from './split-view.css';
@@ -141,3 +141,20 @@ export const SplitView = ({
</div> </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 />; return <PageNotFound noPermission />;
} }
if (!meta) { if (!meta) {
return <AppFallback key="workspaceLoading" />; return <AppFallback />;
} }
return <WorkspacePage meta={meta} />; return <WorkspacePage meta={meta} />;
@@ -204,7 +204,7 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
if (!isRootDocReady) { if (!isRootDocReady) {
return ( return (
<FrameworkScope scope={workspace.scope}> <FrameworkScope scope={workspace.scope}>
<AppFallback key="workspaceLoading" /> <AppFallback />
</FrameworkScope> </FrameworkScope>
); );
} }
+1 -1
View File
@@ -115,7 +115,7 @@ export function App() {
<DebugProvider> <DebugProvider>
<GlobalLoading /> <GlobalLoading />
<RouterProvider <RouterProvider
fallbackElement={<AppFallback key="RouterFallback" />} fallbackElement={<AppFallback />}
router={router} router={router}
future={future} future={future}
/> />
@@ -1,4 +1,4 @@
import { cssVar } from '@toeverything/theme'; import { cssVarV2 } from '@toeverything/theme/v2';
import { createVar, style } from '@vanilla-extract/css'; import { createVar, style } from '@vanilla-extract/css';
export const sidebarOffsetVar = createVar(); export const sidebarOffsetVar = createVar();
@@ -7,10 +7,33 @@ export const root = style({
width: '100vw', width: '100vw',
height: '100vh', height: '100vh',
display: 'flex', display: 'flex',
background: cssVar('backgroundPrimaryColor'), flexDirection: 'column',
background: cssVarV2('layer/background/primary'),
selectors: { selectors: {
'&[data-translucent="true"]': { '&[data-translucent="true"]': {
background: 'transparent', background: 'transparent',
}, },
}, },
}); });
export const appTabsHeader = style({
zIndex: 1,
});
export const fallbackRoot = style({
position: 'absolute',
paddingTop: 52,
width: '100%',
height: '100%',
zIndex: 0,
});
export const splitViewFallback = style({
width: '100%',
height: '100%',
position: 'absolute',
bottom: 0,
right: 0,
zIndex: 0,
background: cssVarV2('layer/background/primary'),
});
@@ -1,5 +1,7 @@
import { AppFallback } from '@affine/core/components/affine/app-container';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header'; import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view';
import * as styles from './shell.css'; import * as styles from './shell.css';
@@ -11,7 +13,10 @@ export function ShellRoot() {
appSettings.enableBlurBackground; appSettings.enableBlurBackground;
return ( return (
<div className={styles.root} data-translucent={translucent}> <div className={styles.root} data-translucent={translucent}>
<AppTabsHeader mode="shell" /> <AppTabsHeader mode="shell" className={styles.appTabsHeader} />
<AppFallback className={styles.fallbackRoot}>
<SplitViewFallback className={styles.splitViewFallback} />
</AppFallback>
</div> </div>
); );
} }
+1 -1
View File
@@ -81,7 +81,7 @@ export function App() {
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<Telemetry /> <Telemetry />
<RouterProvider <RouterProvider
fallbackElement={<AppFallback key="RouterFallback" />} fallbackElement={<AppFallback />}
router={router} router={router}
future={future} future={future}
/> />
@@ -70,7 +70,7 @@ export const WorkspaceLayout = ({
if (!isRootDocReady) { if (!isRootDocReady) {
return ( return (
<FrameworkScope scope={workspace.scope}> <FrameworkScope scope={workspace.scope}>
<AppFallback key="workspaceLoading" /> <AppFallback />
</FrameworkScope> </FrameworkScope>
); );
} }