mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +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:
Vendored
+2
@@ -47,6 +47,7 @@ export type Environment = {
|
|||||||
isElectron: boolean;
|
isElectron: boolean;
|
||||||
isDesktopWeb: boolean;
|
isDesktopWeb: boolean;
|
||||||
isMobileWeb: boolean;
|
isMobileWeb: boolean;
|
||||||
|
isStandalone?: boolean;
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
isLinux: boolean;
|
isLinux: boolean;
|
||||||
@@ -116,6 +117,7 @@ export function setupGlobal() {
|
|||||||
isFireFox: uaHelper.isFireFox,
|
isFireFox: uaHelper.isFireFox,
|
||||||
isChrome: uaHelper.isChrome,
|
isChrome: uaHelper.isChrome,
|
||||||
isIOS: uaHelper.isIOS,
|
isIOS: uaHelper.isIOS,
|
||||||
|
isStandalone: uaHelper.isStandalone,
|
||||||
};
|
};
|
||||||
// Chrome on iOS is still Safari
|
// Chrome on iOS is still Safari
|
||||||
if (environment.isChrome && !environment.isIOS) {
|
if (environment.isChrome && !environment.isIOS) {
|
||||||
|
|||||||
Vendored
+9
@@ -8,6 +8,7 @@ export class UaHelper {
|
|||||||
public isMobile = false;
|
public isMobile = false;
|
||||||
public isChrome = false;
|
public isChrome = false;
|
||||||
public isIOS = false;
|
public isIOS = false;
|
||||||
|
public isStandalone = false;
|
||||||
|
|
||||||
getChromeVersion = (): number => {
|
getChromeVersion = (): number => {
|
||||||
let raw = this.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
let raw = this.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||||
@@ -30,6 +31,13 @@ export class UaHelper {
|
|||||||
return Boolean(this.uaMap[isUseragent]);
|
return Boolean(this.uaMap[isUseragent]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isStandaloneMode() {
|
||||||
|
if ('standalone' in window.navigator) {
|
||||||
|
return !!window.navigator.standalone;
|
||||||
|
}
|
||||||
|
return !!window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
private initUaFlags() {
|
private initUaFlags() {
|
||||||
this.isLinux = this.checkUseragent('linux');
|
this.isLinux = this.checkUseragent('linux');
|
||||||
this.isMacOs = this.checkUseragent('mac');
|
this.isMacOs = this.checkUseragent('mac');
|
||||||
@@ -39,6 +47,7 @@ export class UaHelper {
|
|||||||
this.isMobile = this.checkUseragent('mobile');
|
this.isMobile = this.checkUseragent('mobile');
|
||||||
this.isChrome = this.checkUseragent('chrome');
|
this.isChrome = this.checkUseragent('chrome');
|
||||||
this.isIOS = this.checkUseragent('ios');
|
this.isIOS = this.checkUseragent('ios');
|
||||||
|
this.isStandalone = this.isStandaloneMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export { useAutoFocus, useAutoSelect } from './focus-and-select';
|
export { useAutoFocus, useAutoSelect } from './focus-and-select';
|
||||||
export { useRefEffect } from './use-ref-effect';
|
export { useRefEffect } from './use-ref-effect';
|
||||||
|
export * from './use-theme-color-meta';
|
||||||
|
export * from './use-theme-value';
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { useThemeValueV1, useThemeValueV2 } from './use-theme-value';
|
||||||
|
|
||||||
|
let meta: HTMLMetaElement | null = null;
|
||||||
|
|
||||||
|
function getMeta() {
|
||||||
|
if (meta) return meta;
|
||||||
|
|
||||||
|
const exists = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (exists) {
|
||||||
|
meta = exists as HTMLMetaElement;
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and append meta
|
||||||
|
meta = document.createElement('meta');
|
||||||
|
meta.name = 'theme-color';
|
||||||
|
document.head.append(meta);
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useThemeColorMeta = (color: string) => {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const meta = getMeta();
|
||||||
|
const old = meta.content;
|
||||||
|
meta.content = color;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
meta.content = old;
|
||||||
|
};
|
||||||
|
}, [color]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThemeColorV1 = (
|
||||||
|
...args: Parameters<typeof useThemeValueV1>
|
||||||
|
) => {
|
||||||
|
useThemeColorMeta(useThemeValueV1(...args));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThemeColorV2 = (
|
||||||
|
...args: Parameters<typeof useThemeValueV2>
|
||||||
|
) => {
|
||||||
|
useThemeColorMeta(useThemeValueV2(...args));
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { type AffineTheme, darkTheme, lightTheme } from '@toeverything/theme';
|
||||||
|
import {
|
||||||
|
type AffineThemeKeyV2,
|
||||||
|
darkThemeV2,
|
||||||
|
lightThemeV2,
|
||||||
|
} from '@toeverything/theme/v2';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
|
export const useThemeValueV2 = (key: AffineThemeKeyV2) => {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return resolvedTheme === 'dark' ? darkThemeV2[key] : lightThemeV2[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThemeValueV1 = (key: keyof Omit<AffineTheme, 'editorMode'>) => {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return resolvedTheme === 'dark' ? darkTheme[key] : lightTheme[key];
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ export * from './ui/modal';
|
|||||||
export * from './ui/notification';
|
export * from './ui/notification';
|
||||||
export * from './ui/popover';
|
export * from './ui/popover';
|
||||||
export * from './ui/radio';
|
export * from './ui/radio';
|
||||||
|
export * from './ui/safe-area';
|
||||||
export * from './ui/scrollbar';
|
export * from './ui/scrollbar';
|
||||||
export * from './ui/skeleton';
|
export * from './ui/skeleton';
|
||||||
export * from './ui/slider';
|
export * from './ui/slider';
|
||||||
|
|||||||
@@ -111,10 +111,6 @@ export const MobileMenu = ({
|
|||||||
className: clsx(className, styles.mobileMenuModal),
|
className: clsx(className, styles.mobileMenuModal),
|
||||||
...otherContentOptions,
|
...otherContentOptions,
|
||||||
}}
|
}}
|
||||||
contentWrapperStyle={{
|
|
||||||
alignItems: 'end',
|
|
||||||
paddingBottom: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={setSliderElement}
|
ref={setSliderElement}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { forwardRef, useCallback, useEffect, useState } from 'react';
|
|||||||
import { startScopedViewTransition } from '../../utils';
|
import { startScopedViewTransition } from '../../utils';
|
||||||
import type { IconButtonProps } from '../button';
|
import type { IconButtonProps } from '../button';
|
||||||
import { IconButton } from '../button';
|
import { IconButton } from '../button';
|
||||||
|
import { SafeArea } from '../safe-area';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface ModalProps extends DialogProps {
|
export interface ModalProps extends DialogProps {
|
||||||
@@ -214,7 +215,9 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
}}
|
}}
|
||||||
{...otherOverlayOptions}
|
{...otherOverlayOptions}
|
||||||
>
|
>
|
||||||
<div
|
<SafeArea
|
||||||
|
bottom={environment.isMobileEdition}
|
||||||
|
bottomOffset={12}
|
||||||
data-full-screen={fullScreen}
|
data-full-screen={fullScreen}
|
||||||
data-modal={modal}
|
data-modal={modal}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -278,7 +281,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
|
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</SafeArea>
|
||||||
</Dialog.Overlay>
|
</Dialog.Overlay>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const modalContentWrapper = style({
|
|||||||
'screen and (width <= 640px)': {
|
'screen and (width <= 640px)': {
|
||||||
// todo: adjust animation
|
// todo: adjust animation
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
paddingBottom: 32,
|
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { forwardRef, type HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
import { withUnit } from '../../utils/with-unit';
|
||||||
|
import { bottomOffsetVar, safeArea, topOffsetVar } from './style.css';
|
||||||
|
|
||||||
|
interface SafeAreaProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
top?: boolean;
|
||||||
|
bottom?: boolean;
|
||||||
|
topOffset?: number | string;
|
||||||
|
bottomOffset?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SafeArea = forwardRef<HTMLDivElement, SafeAreaProps>(
|
||||||
|
function SafeArea(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
topOffset = 0,
|
||||||
|
bottomOffset = 0,
|
||||||
|
...attrs
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(safeArea, className)}
|
||||||
|
data-standalone={environment.isStandalone ? '' : undefined}
|
||||||
|
data-bottom={bottom ? '' : undefined}
|
||||||
|
data-top={top ? '' : undefined}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
...assignInlineVars({
|
||||||
|
[topOffsetVar]: withUnit(topOffset, 'px'),
|
||||||
|
[bottomOffsetVar]: withUnit(bottomOffset, 'px'),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const topOffsetVar = createVar();
|
||||||
|
export const bottomOffsetVar = createVar();
|
||||||
|
|
||||||
|
export const safeArea = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-top]': {
|
||||||
|
paddingTop: `calc(${topOffsetVar} + 12px)`,
|
||||||
|
},
|
||||||
|
'&[data-bottom]': {
|
||||||
|
paddingBottom: `calc(${bottomOffsetVar} + 0px)`,
|
||||||
|
},
|
||||||
|
'&[data-standalone][data-top]': {
|
||||||
|
paddingTop: `calc(env(safe-area-inset-top, 12px) + ${topOffsetVar})`,
|
||||||
|
},
|
||||||
|
'&[data-standalone][data-bottom]': {
|
||||||
|
// paddingBottom: 'env(safe-area-inset-bottom, 12px)',
|
||||||
|
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${bottomOffsetVar})`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,3 +1,4 @@
|
|||||||
|
import { SafeArea } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
WorkbenchLink,
|
WorkbenchLink,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
@@ -39,29 +40,31 @@ export const AppTabs = () => {
|
|||||||
const location = useLiveData(workbench.location$);
|
const location = useLiveData(workbench.location$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.appTabs} id="app-tabs" role="tablist">
|
<SafeArea bottom className={styles.appTabs} bottomOffset={2}>
|
||||||
{routes.map(route => {
|
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
|
||||||
const Link = route.LinkComponent || WorkbenchLink;
|
{routes.map(route => {
|
||||||
|
const Link = route.LinkComponent || WorkbenchLink;
|
||||||
|
|
||||||
const isActive = route.isActive
|
const isActive = route.isActive
|
||||||
? route.isActive(location)
|
? route.isActive(location)
|
||||||
: location.pathname === route.to;
|
: location.pathname === route.to;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
to={route.to}
|
to={route.to}
|
||||||
key={route.to}
|
key={route.to}
|
||||||
className={styles.tabItem}
|
className={styles.tabItem}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-label={route.to.slice(1)}
|
aria-label={route.to.slice(1)}
|
||||||
replaceHistory
|
replaceHistory
|
||||||
>
|
>
|
||||||
<li>
|
<li style={{ lineHeight: 0 }}>
|
||||||
<route.Icon />
|
<route.Icon />
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
</SafeArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ import { style } from '@vanilla-extract/css';
|
|||||||
import { globalVars } from '../../styles/mobile.css';
|
import { globalVars } from '../../styles/mobile.css';
|
||||||
|
|
||||||
export const appTabs = style({
|
export const appTabs = style({
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
|
|
||||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
|
||||||
width: '100dvw',
|
width: '100dvw',
|
||||||
height: `calc(${globalVars.appTabHeight} + 2px)`,
|
|
||||||
padding: 16,
|
|
||||||
gap: 15.5,
|
|
||||||
|
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
paddingBottom: 18,
|
|
||||||
bottom: -2,
|
bottom: -2,
|
||||||
zIndex: 1,
|
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({
|
export const tabItem = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton, SafeArea } from '@affine/component';
|
||||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,7 @@ export interface PageHeaderProps
|
|||||||
suffixClassName?: string;
|
suffixClassName?: string;
|
||||||
suffixStyle?: React.CSSProperties;
|
suffixStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||||
function PageHeader(
|
function PageHeader(
|
||||||
{
|
{
|
||||||
back,
|
back,
|
||||||
@@ -65,38 +65,41 @@ export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
|||||||
}, [backAction]);
|
}, [backAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<SafeArea
|
||||||
data-testid="mobile-page-header"
|
top
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(styles.root, className)}
|
className={clsx(styles.root, className)}
|
||||||
|
data-testid="mobile-page-header"
|
||||||
{...attrs}
|
{...attrs}
|
||||||
>
|
>
|
||||||
<section
|
<header className={styles.inner}>
|
||||||
className={clsx(styles.prefix, prefixClassName)}
|
<section
|
||||||
style={prefixStyle}
|
className={clsx(styles.prefix, prefixClassName)}
|
||||||
>
|
style={prefixStyle}
|
||||||
{back ? (
|
>
|
||||||
<IconButton
|
{back ? (
|
||||||
size={24}
|
<IconButton
|
||||||
style={{ padding: 10 }}
|
size={24}
|
||||||
onClick={handleRouteBack}
|
style={{ padding: 10 }}
|
||||||
icon={<ArrowLeftSmallIcon />}
|
onClick={handleRouteBack}
|
||||||
/>
|
icon={<ArrowLeftSmallIcon />}
|
||||||
) : null}
|
/>
|
||||||
{prefix}
|
) : null}
|
||||||
</section>
|
{prefix}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className={clsx(styles.content, { center: centerContent })}>
|
<section className={clsx(styles.content, { center: centerContent })}>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={clsx(styles.suffix, suffixClassName)}
|
className={clsx(styles.suffix, suffixClassName)}
|
||||||
style={suffixStyle}
|
style={suffixStyle}
|
||||||
>
|
>
|
||||||
{suffix}
|
{suffix}
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
</SafeArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { style } from '@vanilla-extract/css';
|
|||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: 44,
|
|
||||||
padding: '0 6px',
|
|
||||||
paddingTop: 16,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
|
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
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({
|
export const content = style({
|
||||||
selectors: {
|
selectors: {
|
||||||
'&.center': {
|
'&.center': {
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||||
|
|
||||||
import { AppTabs } from '../../components';
|
import { AppTabs } from '../../components';
|
||||||
import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views';
|
import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AllDocsHeader operations={<AllDocsMenu />} />
|
<AllDocsHeader operations={<AllDocsMenu />} />
|
||||||
<AllDocList />
|
<SafeArea bottom>
|
||||||
|
<AllDocList />
|
||||||
|
</SafeArea>
|
||||||
<AppTabs />
|
<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 { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
|
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
|
||||||
@@ -16,6 +16,7 @@ import { AppTabs } from '../../../components';
|
|||||||
import { CollectionDetail, EmptyCollection } from '../../../views';
|
import { CollectionDetail, EmptyCollection } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
const { collectionService, globalContextService, workspaceService } =
|
const { collectionService, globalContextService, workspaceService } =
|
||||||
useServices({
|
useServices({
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useThemeColorV2 } from '@affine/component';
|
||||||
|
|
||||||
import { AppTabs } from '../../../components';
|
import { AppTabs } from '../../../components';
|
||||||
import { AllDocsHeader, CollectionList } from '../../../views';
|
import { AllDocsHeader, CollectionList } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AllDocsHeader />
|
<AllDocsHeader />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useThemeColorV2 } from '@affine/component';
|
||||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
@@ -213,6 +214,7 @@ const notFound = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/primary');
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const pageId = params.pageId;
|
const pageId = params.pageId;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
ExplorerCollections,
|
ExplorerCollections,
|
||||||
ExplorerFavorites,
|
ExplorerFavorites,
|
||||||
@@ -11,24 +12,28 @@ import { AppTabs } from '../../components';
|
|||||||
import { HomeHeader, RecentDocs } from '../../views';
|
import { HomeHeader, RecentDocs } from '../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerMobileContext.Provider value={true}>
|
<ExplorerMobileContext.Provider value={true}>
|
||||||
<HomeHeader />
|
<HomeHeader />
|
||||||
<RecentDocs />
|
<RecentDocs />
|
||||||
<div
|
<SafeArea bottom>
|
||||||
style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
gap: 32,
|
flexDirection: 'column',
|
||||||
padding: '0 8px 32px 8px',
|
gap: 32,
|
||||||
}}
|
padding: '0 8px 32px 8px',
|
||||||
>
|
}}
|
||||||
<ExplorerFavorites />
|
>
|
||||||
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
<ExplorerFavorites />
|
||||||
<ExplorerMigrationFavorites />
|
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
|
||||||
<ExplorerCollections />
|
<ExplorerMigrationFavorites />
|
||||||
<ExplorerTags />
|
<ExplorerCollections />
|
||||||
</div>
|
<ExplorerTags />
|
||||||
|
</div>
|
||||||
|
</SafeArea>
|
||||||
<AppTabs />
|
<AppTabs />
|
||||||
</ExplorerMobileContext.Provider>
|
</ExplorerMobileContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import {
|
import {
|
||||||
type QuickSearchItem,
|
type QuickSearchItem,
|
||||||
@@ -112,6 +113,7 @@ const WithQueryList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
const searchInput = useLiveData(searchInput$);
|
const searchInput = useLiveData(searchInput$);
|
||||||
const searchService = useService(MobileSearchService);
|
const searchService = useService(MobileSearchService);
|
||||||
|
|
||||||
@@ -133,15 +135,17 @@ export const Component = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.searchHeader} data-testid="search-header">
|
<SafeArea top>
|
||||||
<SearchInput
|
<div className={styles.searchHeader} data-testid="search-header">
|
||||||
debounce={300}
|
<SearchInput
|
||||||
autoFocus={!searchInput}
|
debounce={300}
|
||||||
value={searchInput}
|
autoFocus={!searchInput}
|
||||||
onInput={onSearch}
|
value={searchInput}
|
||||||
placeholder="Search Docs, Collections"
|
onInput={onSearch}
|
||||||
/>
|
placeholder="Search Docs, Collections"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</SafeArea>
|
||||||
{searchInput ? <WithQueryList /> : <RecentList />}
|
{searchInput ? <WithQueryList /> : <RecentList />}
|
||||||
<AppTabs />
|
<AppTabs />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useThemeColorV2 } from '@affine/component';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { PageNotFound } from '@affine/core/pages/404';
|
import { PageNotFound } from '@affine/core/pages/404';
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +12,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { TagDetail } from '../../../views';
|
import { TagDetail } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const tagId = params.tagId;
|
const tagId = params.tagId;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useThemeColorV2 } from '@affine/component';
|
||||||
|
|
||||||
import { AppTabs } from '../../../components';
|
import { AppTabs } from '../../../components';
|
||||||
import { AllDocsHeader, TagList } from '../../../views';
|
import { AllDocsHeader, TagList } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/secondary');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AllDocsHeader />
|
<AllDocsHeader />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ globalStyle(':root', {
|
|||||||
|
|
||||||
globalStyle('body', {
|
globalStyle('body', {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
minHeight: '100dvh',
|
||||||
});
|
});
|
||||||
globalStyle('body:has(#app-tabs)', {
|
globalStyle('body:has(#app-tabs)', {
|
||||||
paddingBottom: globalVars.appTabHeight,
|
paddingBottom: globalVars.appTabHeight,
|
||||||
@@ -21,6 +22,3 @@ globalStyle('html', {
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
background: cssVarV2('layer/background/secondary'),
|
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 { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
import { header, headerSpace } from './style.css';
|
import { header, headerContent, headerSpace } from './style.css';
|
||||||
import { AllDocsTabs } from './tabs';
|
import { AllDocsTabs } from './tabs';
|
||||||
|
|
||||||
export interface AllDocsHeaderProps {
|
export interface AllDocsHeaderProps {
|
||||||
@@ -11,17 +11,21 @@ export interface AllDocsHeaderProps {
|
|||||||
export const AllDocsHeader = ({ operations }: AllDocsHeaderProps) => {
|
export const AllDocsHeader = ({ operations }: AllDocsHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={header}>
|
<SafeArea top className={header}>
|
||||||
<AllDocsTabs />
|
<header className={headerContent}>
|
||||||
<div>
|
<AllDocsTabs />
|
||||||
{operations ? (
|
<div>
|
||||||
<MobileMenu items={operations}>
|
{operations ? (
|
||||||
<IconButton icon={<MoreHorizontalIcon />} />
|
<MobileMenu items={operations}>
|
||||||
</MobileMenu>
|
<IconButton icon={<MoreHorizontalIcon />} />
|
||||||
) : null}
|
</MobileMenu>
|
||||||
</div>
|
) : null}
|
||||||
</header>
|
</div>
|
||||||
<div className={headerSpace} />
|
</header>
|
||||||
|
</SafeArea>
|
||||||
|
<SafeArea top>
|
||||||
|
<div className={headerSpace} />
|
||||||
|
</SafeArea>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
const headerContentHeight = 56;
|
|
||||||
const headerPaddingTop = 16;
|
|
||||||
|
|
||||||
const basicHeader = style({
|
const basicHeader = style({
|
||||||
width: '100%',
|
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,
|
basicHeader,
|
||||||
{
|
{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
padding: `${headerPaddingTop}px 16px 0px 16px`,
|
padding: `0px 16px`,
|
||||||
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
export const headerSpace = style([basicHeader]);
|
|
||||||
|
|
||||||
export const tabs = style({
|
export const tabs = style({
|
||||||
height: headerContentHeight,
|
height: 56,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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 { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -41,7 +45,7 @@ export const HomeHeader = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.root, { dense })}>
|
<div className={clsx(styles.root, { dense })}>
|
||||||
<div className={styles.float}>
|
<SafeArea top className={styles.float}>
|
||||||
<div className={styles.headerAndWsSelector}>
|
<div className={styles.headerAndWsSelector}>
|
||||||
<div className={styles.wsSelectorWrapper}>
|
<div className={styles.wsSelectorWrapper}>
|
||||||
<WorkspaceSelector />
|
<WorkspaceSelector />
|
||||||
@@ -60,8 +64,10 @@ export const HomeHeader = () => {
|
|||||||
<div className={styles.searchWrapper}>
|
<div className={styles.searchWrapper}>
|
||||||
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SafeArea>
|
||||||
<div className={styles.space} />
|
<SafeArea top>
|
||||||
|
<div className={styles.space} />
|
||||||
|
</SafeArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export const float = style({
|
|||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: cssVarV2('layer/background/secondary'),
|
background: cssVarV2('layer/background/secondary'),
|
||||||
paddingTop: 12,
|
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
export const space = style({
|
export const space = style({
|
||||||
|
|||||||
@@ -4,8 +4,16 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=<%= VIEWPORT_FIT %>"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta
|
||||||
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
|
content="black-translucent"
|
||||||
|
/>
|
||||||
|
|
||||||
<title>AFFiNE</title>
|
<title>AFFiNE</title>
|
||||||
<meta name="theme-color" content="#fafafa" />
|
<meta name="theme-color" content="#fafafa" />
|
||||||
<link rel="preconnect" href="<%= PUBLIC_PATH %>" />
|
<link rel="preconnect" href="<%= PUBLIC_PATH %>" />
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export function createWebpackConfig(cwd: string, flags: BuildFlags) {
|
|||||||
GIT_SHORT_SHA: gitShortHash(),
|
GIT_SHORT_SHA: gitShortHash(),
|
||||||
DESCRIPTION,
|
DESCRIPTION,
|
||||||
PUBLIC_PATH: config.output?.publicPath,
|
PUBLIC_PATH: config.output?.publicPath,
|
||||||
|
VIEWPORT_FIT: flags.distribution === 'mobile' ? 'cover' : 'auto',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user