mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00: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,2 +1,4 @@
|
||||
export { useAutoFocus, useAutoSelect } from './focus-and-select';
|
||||
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));
|
||||
};
|
||||
19
packages/frontend/component/src/hooks/use-theme-value.ts
Normal file
19
packages/frontend/component/src/hooks/use-theme-value.ts
Normal file
@@ -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/popover';
|
||||
export * from './ui/radio';
|
||||
export * from './ui/safe-area';
|
||||
export * from './ui/scrollbar';
|
||||
export * from './ui/skeleton';
|
||||
export * from './ui/slider';
|
||||
|
||||
@@ -111,10 +111,6 @@ export const MobileMenu = ({
|
||||
className: clsx(className, styles.mobileMenuModal),
|
||||
...otherContentOptions,
|
||||
}}
|
||||
contentWrapperStyle={{
|
||||
alignItems: 'end',
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={setSliderElement}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { forwardRef, useCallback, useEffect, useState } from 'react';
|
||||
import { startScopedViewTransition } from '../../utils';
|
||||
import type { IconButtonProps } from '../button';
|
||||
import { IconButton } from '../button';
|
||||
import { SafeArea } from '../safe-area';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface ModalProps extends DialogProps {
|
||||
@@ -214,7 +215,9 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
}}
|
||||
{...otherOverlayOptions}
|
||||
>
|
||||
<div
|
||||
<SafeArea
|
||||
bottom={environment.isMobileEdition}
|
||||
bottomOffset={12}
|
||||
data-full-screen={fullScreen}
|
||||
data-modal={modal}
|
||||
className={clsx(
|
||||
@@ -278,7 +281,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
|
||||
{children}
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</SafeArea>
|
||||
</Dialog.Overlay>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const modalContentWrapper = style({
|
||||
'screen and (width <= 640px)': {
|
||||
// todo: adjust animation
|
||||
alignItems: 'flex-end',
|
||||
paddingBottom: 32,
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
49
packages/frontend/component/src/ui/safe-area/index.tsx
Normal file
49
packages/frontend/component/src/ui/safe-area/index.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
22
packages/frontend/component/src/ui/safe-area/style.css.ts
Normal file
22
packages/frontend/component/src/ui/safe-area/style.css.ts
Normal file
@@ -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})`,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user