Skip to content

Commit 81ab8ac

Browse files
committed
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
1 parent 9038592 commit 81ab8ac

File tree

31 files changed

+328
-132
lines changed

31 files changed

+328
-132
lines changed

packages/common/env/src/global.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export type Environment = {
4747
isElectron: boolean;
4848
isDesktopWeb: boolean;
4949
isMobileWeb: boolean;
50+
isStandalone?: boolean;
5051

5152
// Device
5253
isLinux: boolean;
@@ -116,6 +117,7 @@ export function setupGlobal() {
116117
isFireFox: uaHelper.isFireFox,
117118
isChrome: uaHelper.isChrome,
118119
isIOS: uaHelper.isIOS,
120+
isStandalone: uaHelper.isStandalone,
119121
};
120122
// Chrome on iOS is still Safari
121123
if (environment.isChrome && !environment.isIOS) {

packages/common/env/src/ua-helper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class UaHelper {
88
public isMobile = false;
99
public isChrome = false;
1010
public isIOS = false;
11+
public isStandalone = false;
1112

1213
getChromeVersion = (): number => {
1314
let raw = this.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
@@ -30,6 +31,13 @@ export class UaHelper {
3031
return Boolean(this.uaMap[isUseragent]);
3132
}
3233

34+
private isStandaloneMode() {
35+
if ('standalone' in window.navigator) {
36+
return !!window.navigator.standalone;
37+
}
38+
return !!window.matchMedia('(display-mode: standalone)').matches;
39+
}
40+
3341
private initUaFlags() {
3442
this.isLinux = this.checkUseragent('linux');
3543
this.isMacOs = this.checkUseragent('mac');
@@ -39,6 +47,7 @@ export class UaHelper {
3947
this.isMobile = this.checkUseragent('mobile');
4048
this.isChrome = this.checkUseragent('chrome');
4149
this.isIOS = this.checkUseragent('ios');
50+
this.isStandalone = this.isStandaloneMode();
4251
}
4352
}
4453

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { useAutoFocus, useAutoSelect } from './focus-and-select';
22
export { useRefEffect } from './use-ref-effect';
3+
export * from './use-theme-color-meta';
4+
export * from './use-theme-value';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useLayoutEffect } from 'react';
2+
3+
import { useThemeValueV1, useThemeValueV2 } from './use-theme-value';
4+
5+
let meta: HTMLMetaElement | null = null;
6+
7+
function getMeta() {
8+
if (meta) return meta;
9+
10+
const exists = document.querySelector('meta[name="theme-color"]');
11+
if (exists) {
12+
meta = exists as HTMLMetaElement;
13+
return meta;
14+
}
15+
16+
// create and append meta
17+
meta = document.createElement('meta');
18+
meta.name = 'theme-color';
19+
document.head.append(meta);
20+
return meta;
21+
}
22+
23+
export const useThemeColorMeta = (color: string) => {
24+
useLayoutEffect(() => {
25+
const meta = getMeta();
26+
const old = meta.content;
27+
meta.content = color;
28+
29+
return () => {
30+
meta.content = old;
31+
};
32+
}, [color]);
33+
};
34+
35+
export const useThemeColorV1 = (
36+
...args: Parameters<typeof useThemeValueV1>
37+
) => {
38+
useThemeColorMeta(useThemeValueV1(...args));
39+
};
40+
41+
export const useThemeColorV2 = (
42+
...args: Parameters<typeof useThemeValueV2>
43+
) => {
44+
useThemeColorMeta(useThemeValueV2(...args));
45+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type AffineTheme, darkTheme, lightTheme } from '@toeverything/theme';
2+
import {
3+
type AffineThemeKeyV2,
4+
darkThemeV2,
5+
lightThemeV2,
6+
} from '@toeverything/theme/v2';
7+
import { useTheme } from 'next-themes';
8+
9+
export const useThemeValueV2 = (key: AffineThemeKeyV2) => {
10+
const { resolvedTheme } = useTheme();
11+
12+
return resolvedTheme === 'dark' ? darkThemeV2[key] : lightThemeV2[key];
13+
};
14+
15+
export const useThemeValueV1 = (key: keyof Omit<AffineTheme, 'editorMode'>) => {
16+
const { resolvedTheme } = useTheme();
17+
18+
return resolvedTheme === 'dark' ? darkTheme[key] : lightTheme[key];
19+
};

packages/frontend/component/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './ui/modal';
2121
export * from './ui/notification';
2222
export * from './ui/popover';
2323
export * from './ui/radio';
24+
export * from './ui/safe-area';
2425
export * from './ui/scrollbar';
2526
export * from './ui/skeleton';
2627
export * from './ui/slider';

packages/frontend/component/src/ui/menu/mobile/root.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ export const MobileMenu = ({
111111
className: clsx(className, styles.mobileMenuModal),
112112
...otherContentOptions,
113113
}}
114-
contentWrapperStyle={{
115-
alignItems: 'end',
116-
paddingBottom: 10,
117-
}}
118114
>
119115
<div
120116
ref={setSliderElement}

packages/frontend/component/src/ui/modal/modal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { forwardRef, useCallback, useEffect, useState } from 'react';
1515
import { startScopedViewTransition } from '../../utils';
1616
import type { IconButtonProps } from '../button';
1717
import { IconButton } from '../button';
18+
import { SafeArea } from '../safe-area';
1819
import * as styles from './styles.css';
1920

2021
export interface ModalProps extends DialogProps {
@@ -214,7 +215,9 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
214215
}}
215216
{...otherOverlayOptions}
216217
>
217-
<div
218+
<SafeArea
219+
bottom={environment.isMobileEdition}
220+
bottomOffset={12}
218221
data-full-screen={fullScreen}
219222
data-modal={modal}
220223
className={clsx(
@@ -278,7 +281,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
278281

279282
{children}
280283
</Dialog.Content>
281-
</div>
284+
</SafeArea>
282285
</Dialog.Overlay>
283286
</Dialog.Portal>
284287
</Dialog.Root>

packages/frontend/component/src/ui/modal/styles.css.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const modalContentWrapper = style({
8383
'screen and (width <= 640px)': {
8484
// todo: adjust animation
8585
alignItems: 'flex-end',
86-
paddingBottom: 32,
86+
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
8787
},
8888
},
8989

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { assignInlineVars } from '@vanilla-extract/dynamic';
2+
import clsx from 'clsx';
3+
import { forwardRef, type HTMLAttributes } from 'react';
4+
5+
import { withUnit } from '../../utils/with-unit';
6+
import { bottomOffsetVar, safeArea, topOffsetVar } from './style.css';
7+
8+
interface SafeAreaProps extends HTMLAttributes<HTMLDivElement> {
9+
top?: boolean;
10+
bottom?: boolean;
11+
topOffset?: number | string;
12+
bottomOffset?: number | string;
13+
}
14+
15+
export const SafeArea = forwardRef<HTMLDivElement, SafeAreaProps>(
16+
function SafeArea(
17+
{
18+
children,
19+
className,
20+
style,
21+
top,
22+
bottom,
23+
topOffset = 0,
24+
bottomOffset = 0,
25+
...attrs
26+
},
27+
ref
28+
) {
29+
return (
30+
<div
31+
ref={ref}
32+
className={clsx(safeArea, className)}
33+
data-standalone={environment.isStandalone ? '' : undefined}
34+
data-bottom={bottom ? '' : undefined}
35+
data-top={top ? '' : undefined}
36+
style={{
37+
...style,
38+
...assignInlineVars({
39+
[topOffsetVar]: withUnit(topOffset, 'px'),
40+
[bottomOffsetVar]: withUnit(bottomOffset, 'px'),
41+
}),
42+
}}
43+
{...attrs}
44+
>
45+
{children}
46+
</div>
47+
);
48+
}
49+
);

0 commit comments

Comments
 (0)