mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat(mobile): pwa and browser theme-color optimization (#8168)
[AF-1325](https://linear.app/affine-design/issue/AF-1325/优化-pwa-体验), [AF-1317](https://linear.app/affine-design/issue/AF-1317/优化:-pwa-的顶部-status-bar-颜色应与背景保持一致), [AF-1318](https://linear.app/affine-design/issue/AF-1318/优化:pwa-的底部应当有符合设备安全高度的padding), [AF-1321](https://linear.app/affine-design/issue/AF-1321/更新一下-fail-的-pwa-icon) - New `<SafeArea />` ui component - New `useThemeColorV1` / `useThemeColorV2` hook: - to modify `<meta name="theme-color" />` with given theme key
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { SafeArea } from '@affine/component';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
WorkbenchService,
|
||||
@@ -39,29 +40,31 @@ export const AppTabs = () => {
|
||||
const location = useLiveData(workbench.location$);
|
||||
|
||||
return (
|
||||
<ul className={styles.appTabs} id="app-tabs" role="tablist">
|
||||
{routes.map(route => {
|
||||
const Link = route.LinkComponent || WorkbenchLink;
|
||||
<SafeArea bottom className={styles.appTabs} bottomOffset={2}>
|
||||
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
|
||||
{routes.map(route => {
|
||||
const Link = route.LinkComponent || WorkbenchLink;
|
||||
|
||||
const isActive = route.isActive
|
||||
? route.isActive(location)
|
||||
: location.pathname === route.to;
|
||||
return (
|
||||
<Link
|
||||
data-active={isActive}
|
||||
to={route.to}
|
||||
key={route.to}
|
||||
className={styles.tabItem}
|
||||
role="tab"
|
||||
aria-label={route.to.slice(1)}
|
||||
replaceHistory
|
||||
>
|
||||
<li>
|
||||
<route.Icon />
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
const isActive = route.isActive
|
||||
? route.isActive(location)
|
||||
: location.pathname === route.to;
|
||||
return (
|
||||
<Link
|
||||
data-active={isActive}
|
||||
to={route.to}
|
||||
key={route.to}
|
||||
className={styles.tabItem}
|
||||
role="tab"
|
||||
aria-label={route.to.slice(1)}
|
||||
replaceHistory
|
||||
>
|
||||
<li style={{ lineHeight: 0 }}>
|
||||
<route.Icon />
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</SafeArea>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,23 +4,24 @@ import { style } from '@vanilla-extract/css';
|
||||
import { globalVars } from '../../styles/mobile.css';
|
||||
|
||||
export const appTabs = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
|
||||
width: '100dvw',
|
||||
height: `calc(${globalVars.appTabHeight} + 2px)`,
|
||||
padding: 16,
|
||||
gap: 15.5,
|
||||
|
||||
position: 'fixed',
|
||||
paddingBottom: 18,
|
||||
bottom: -2,
|
||||
zIndex: 1,
|
||||
});
|
||||
export const appTabsInner = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 15.5,
|
||||
|
||||
height: `calc(${globalVars.appTabHeight} + 2px)`,
|
||||
padding: 16,
|
||||
});
|
||||
export const tabItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { IconButton, SafeArea } from '@affine/component';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ export interface PageHeaderProps
|
||||
suffixClassName?: string;
|
||||
suffixStyle?: React.CSSProperties;
|
||||
}
|
||||
export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
||||
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
function PageHeader(
|
||||
{
|
||||
back,
|
||||
@@ -65,38 +65,41 @@ export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
||||
}, [backAction]);
|
||||
|
||||
return (
|
||||
<header
|
||||
data-testid="mobile-page-header"
|
||||
<SafeArea
|
||||
top
|
||||
ref={ref}
|
||||
className={clsx(styles.root, className)}
|
||||
data-testid="mobile-page-header"
|
||||
{...attrs}
|
||||
>
|
||||
<section
|
||||
className={clsx(styles.prefix, prefixClassName)}
|
||||
style={prefixStyle}
|
||||
>
|
||||
{back ? (
|
||||
<IconButton
|
||||
size={24}
|
||||
style={{ padding: 10 }}
|
||||
onClick={handleRouteBack}
|
||||
icon={<ArrowLeftSmallIcon />}
|
||||
/>
|
||||
) : null}
|
||||
{prefix}
|
||||
</section>
|
||||
<header className={styles.inner}>
|
||||
<section
|
||||
className={clsx(styles.prefix, prefixClassName)}
|
||||
style={prefixStyle}
|
||||
>
|
||||
{back ? (
|
||||
<IconButton
|
||||
size={24}
|
||||
style={{ padding: 10 }}
|
||||
onClick={handleRouteBack}
|
||||
icon={<ArrowLeftSmallIcon />}
|
||||
/>
|
||||
) : null}
|
||||
{prefix}
|
||||
</section>
|
||||
|
||||
<section className={clsx(styles.content, { center: centerContent })}>
|
||||
{children}
|
||||
</section>
|
||||
<section className={clsx(styles.content, { center: centerContent })}>
|
||||
{children}
|
||||
</section>
|
||||
|
||||
<section
|
||||
className={clsx(styles.suffix, suffixClassName)}
|
||||
style={suffixStyle}
|
||||
>
|
||||
{suffix}
|
||||
</section>
|
||||
</header>
|
||||
<section
|
||||
className={clsx(styles.suffix, suffixClassName)}
|
||||
style={suffixStyle}
|
||||
>
|
||||
{suffix}
|
||||
</section>
|
||||
</header>
|
||||
</SafeArea>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,18 +3,18 @@ import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
width: '100%',
|
||||
minHeight: 44,
|
||||
padding: '0 6px',
|
||||
paddingTop: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
});
|
||||
export const inner = style({
|
||||
minHeight: 44,
|
||||
padding: '0 6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const content = style({
|
||||
selectors: {
|
||||
'&.center': {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||
|
||||
import { AppTabs } from '../../components';
|
||||
import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
|
||||
return (
|
||||
<>
|
||||
<AllDocsHeader operations={<AllDocsMenu />} />
|
||||
<AllDocList />
|
||||
<SafeArea bottom>
|
||||
<AllDocList />
|
||||
</SafeArea>
|
||||
<AppTabs />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { notify, useThemeColorV2 } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
|
||||
@@ -16,6 +16,7 @@ import { AppTabs } from '../../../components';
|
||||
import { CollectionDetail, EmptyCollection } from '../../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
const { collectionService, globalContextService, workspaceService } =
|
||||
useServices({
|
||||
WorkspaceService,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useThemeColorV2 } from '@affine/component';
|
||||
|
||||
import { AppTabs } from '../../../components';
|
||||
import { AllDocsHeader, CollectionList } from '../../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
return (
|
||||
<>
|
||||
<AllDocsHeader />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useThemeColorV2 } from '@affine/component';
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||
@@ -213,6 +214,7 @@ const notFound = (
|
||||
);
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/primary');
|
||||
const params = useParams();
|
||||
const pageId = params.pageId;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||
import {
|
||||
ExplorerCollections,
|
||||
ExplorerFavorites,
|
||||
@@ -11,24 +12,28 @@ import { AppTabs } from '../../components';
|
||||
import { HomeHeader, RecentDocs } from '../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
|
||||
return (
|
||||
<ExplorerMobileContext.Provider value={true}>
|
||||
<HomeHeader />
|
||||
<RecentDocs />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 32,
|
||||
padding: '0 8px 32px 8px',
|
||||
}}
|
||||
>
|
||||
<ExplorerFavorites />
|
||||
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
||||
<ExplorerMigrationFavorites />
|
||||
<ExplorerCollections />
|
||||
<ExplorerTags />
|
||||
</div>
|
||||
<SafeArea bottom>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 32,
|
||||
padding: '0 8px 32px 8px',
|
||||
}}
|
||||
>
|
||||
<ExplorerFavorites />
|
||||
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
||||
<ExplorerMigrationFavorites />
|
||||
<ExplorerCollections />
|
||||
<ExplorerTags />
|
||||
</div>
|
||||
</SafeArea>
|
||||
<AppTabs />
|
||||
</ExplorerMobileContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type QuickSearchItem,
|
||||
@@ -112,6 +113,7 @@ const WithQueryList = () => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
const searchInput = useLiveData(searchInput$);
|
||||
const searchService = useService(MobileSearchService);
|
||||
|
||||
@@ -133,15 +135,17 @@ export const Component = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.searchHeader} data-testid="search-header">
|
||||
<SearchInput
|
||||
debounce={300}
|
||||
autoFocus={!searchInput}
|
||||
value={searchInput}
|
||||
onInput={onSearch}
|
||||
placeholder="Search Docs, Collections"
|
||||
/>
|
||||
</div>
|
||||
<SafeArea top>
|
||||
<div className={styles.searchHeader} data-testid="search-header">
|
||||
<SearchInput
|
||||
debounce={300}
|
||||
autoFocus={!searchInput}
|
||||
value={searchInput}
|
||||
onInput={onSearch}
|
||||
placeholder="Search Docs, Collections"
|
||||
/>
|
||||
</div>
|
||||
</SafeArea>
|
||||
{searchInput ? <WithQueryList /> : <RecentList />}
|
||||
<AppTabs />
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useThemeColorV2 } from '@affine/component';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { PageNotFound } from '@affine/core/pages/404';
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { TagDetail } from '../../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
const params = useParams();
|
||||
const tagId = params.tagId;
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useThemeColorV2 } from '@affine/component';
|
||||
|
||||
import { AppTabs } from '../../../components';
|
||||
import { AllDocsHeader, TagList } from '../../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
return (
|
||||
<>
|
||||
<AllDocsHeader />
|
||||
|
||||
@@ -13,6 +13,7 @@ globalStyle(':root', {
|
||||
|
||||
globalStyle('body', {
|
||||
height: 'auto',
|
||||
minHeight: '100dvh',
|
||||
});
|
||||
globalStyle('body:has(#app-tabs)', {
|
||||
paddingBottom: globalVars.appTabHeight,
|
||||
@@ -21,6 +22,3 @@ globalStyle('html', {
|
||||
overflowY: 'auto',
|
||||
background: cssVarV2('layer/background/secondary'),
|
||||
});
|
||||
globalStyle('body[data-scroll-locked][style]', {
|
||||
overflow: 'clip !important',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton, MobileMenu } from '@affine/component';
|
||||
import { IconButton, MobileMenu, SafeArea } from '@affine/component';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { header, headerSpace } from './style.css';
|
||||
import { header, headerContent, headerSpace } from './style.css';
|
||||
import { AllDocsTabs } from './tabs';
|
||||
|
||||
export interface AllDocsHeaderProps {
|
||||
@@ -11,17 +11,21 @@ export interface AllDocsHeaderProps {
|
||||
export const AllDocsHeader = ({ operations }: AllDocsHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<header className={header}>
|
||||
<AllDocsTabs />
|
||||
<div>
|
||||
{operations ? (
|
||||
<MobileMenu items={operations}>
|
||||
<IconButton icon={<MoreHorizontalIcon />} />
|
||||
</MobileMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
<div className={headerSpace} />
|
||||
<SafeArea top className={header}>
|
||||
<header className={headerContent}>
|
||||
<AllDocsTabs />
|
||||
<div>
|
||||
{operations ? (
|
||||
<MobileMenu items={operations}>
|
||||
<IconButton icon={<MoreHorizontalIcon />} />
|
||||
</MobileMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
</SafeArea>
|
||||
<SafeArea top>
|
||||
<div className={headerSpace} />
|
||||
</SafeArea>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const headerContentHeight = 56;
|
||||
const headerPaddingTop = 16;
|
||||
|
||||
const basicHeader = style({
|
||||
width: '100%',
|
||||
height: headerContentHeight + headerPaddingTop,
|
||||
height: 56,
|
||||
});
|
||||
export const header = style([
|
||||
export const header = style({
|
||||
width: '100%',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
zIndex: 1,
|
||||
});
|
||||
export const headerSpace = style([basicHeader]);
|
||||
export const headerContent = style([
|
||||
basicHeader,
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
padding: `${headerPaddingTop}px 16px 0px 16px`,
|
||||
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
zIndex: 1,
|
||||
padding: `0px 16px`,
|
||||
},
|
||||
]);
|
||||
export const headerSpace = style([basicHeader]);
|
||||
|
||||
export const tabs = style({
|
||||
height: headerContentHeight,
|
||||
height: 56,
|
||||
gap: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { IconButton, startScopedViewTransition } from '@affine/component';
|
||||
import {
|
||||
IconButton,
|
||||
SafeArea,
|
||||
startScopedViewTransition,
|
||||
} from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -41,7 +45,7 @@ export const HomeHeader = () => {
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.root, { dense })}>
|
||||
<div className={styles.float}>
|
||||
<SafeArea top className={styles.float}>
|
||||
<div className={styles.headerAndWsSelector}>
|
||||
<div className={styles.wsSelectorWrapper}>
|
||||
<WorkspaceSelector />
|
||||
@@ -60,8 +64,10 @@ export const HomeHeader = () => {
|
||||
<div className={styles.searchWrapper}>
|
||||
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.space} />
|
||||
</SafeArea>
|
||||
<SafeArea top>
|
||||
<div className={styles.space} />
|
||||
</SafeArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ export const float = style({
|
||||
top: 0,
|
||||
width: '100%',
|
||||
background: cssVarV2('layer/background/secondary'),
|
||||
paddingTop: 12,
|
||||
zIndex: 1,
|
||||
});
|
||||
export const space = style({
|
||||
|
||||
Reference in New Issue
Block a user