From 81ab8ac8b3b3911dca7f013c5be82ac9a26f97ea Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Wed, 11 Sep 2024 02:20:59 +0000 Subject: [PATCH] feat(mobile): pwa and browser theme-color optimization (#8168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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 `` ui component - New `useThemeColorV1` / `useThemeColorV2` hook: - to modify `` with given theme key --- packages/common/env/src/global.ts | 2 + packages/common/env/src/ua-helper.ts | 9 +++ .../frontend/component/src/hooks/index.ts | 2 + .../src/hooks/use-theme-color-meta.ts | 45 +++++++++++++ .../component/src/hooks/use-theme-value.ts | 19 ++++++ packages/frontend/component/src/index.ts | 1 + .../component/src/ui/menu/mobile/root.tsx | 4 -- .../frontend/component/src/ui/modal/modal.tsx | 7 ++- .../component/src/ui/modal/styles.css.ts | 2 +- .../component/src/ui/safe-area/index.tsx | 49 +++++++++++++++ .../component/src/ui/safe-area/style.css.ts | 22 +++++++ .../frontend/core/public/apple-touch-icon.png | Bin 5580 -> 19657 bytes .../mobile/src/components/app-tabs/index.tsx | 49 ++++++++------- .../src/components/app-tabs/styles.css.ts | 17 ++--- .../src/components/page-header/index.tsx | 59 +++++++++--------- .../src/components/page-header/styles.css.ts | 14 ++--- .../mobile/src/pages/workspace/all.tsx | 8 ++- .../src/pages/workspace/collection/detail.tsx | 3 +- .../src/pages/workspace/collection/index.tsx | 3 + .../workspace/detail/mobile-detail-page.tsx | 2 + .../mobile/src/pages/workspace/home.tsx | 33 +++++----- .../mobile/src/pages/workspace/search.tsx | 22 ++++--- .../mobile/src/pages/workspace/tag/detail.tsx | 2 + .../mobile/src/pages/workspace/tag/index.tsx | 3 + .../frontend/mobile/src/styles/mobile.css.ts | 4 +- .../mobile/src/views/all-docs/header.tsx | 30 +++++---- .../mobile/src/views/all-docs/style.css.ts | 25 ++++---- .../mobile/src/views/home-header/index.tsx | 14 +++-- .../src/views/home-header/styles.css.ts | 1 - tools/cli/src/webpack/template.html | 10 ++- tools/cli/src/webpack/webpack.config.ts | 1 + 31 files changed, 329 insertions(+), 133 deletions(-) create mode 100644 packages/frontend/component/src/hooks/use-theme-color-meta.ts create mode 100644 packages/frontend/component/src/hooks/use-theme-value.ts create mode 100644 packages/frontend/component/src/ui/safe-area/index.tsx create mode 100644 packages/frontend/component/src/ui/safe-area/style.css.ts diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts index 21fbdbf814..dc694d55e1 100644 --- a/packages/common/env/src/global.ts +++ b/packages/common/env/src/global.ts @@ -47,6 +47,7 @@ export type Environment = { isElectron: boolean; isDesktopWeb: boolean; isMobileWeb: boolean; + isStandalone?: boolean; // Device isLinux: boolean; @@ -116,6 +117,7 @@ export function setupGlobal() { isFireFox: uaHelper.isFireFox, isChrome: uaHelper.isChrome, isIOS: uaHelper.isIOS, + isStandalone: uaHelper.isStandalone, }; // Chrome on iOS is still Safari if (environment.isChrome && !environment.isIOS) { diff --git a/packages/common/env/src/ua-helper.ts b/packages/common/env/src/ua-helper.ts index 8a0322fcfb..f1149f00be 100644 --- a/packages/common/env/src/ua-helper.ts +++ b/packages/common/env/src/ua-helper.ts @@ -8,6 +8,7 @@ export class UaHelper { public isMobile = false; public isChrome = false; public isIOS = false; + public isStandalone = false; getChromeVersion = (): number => { let raw = this.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); @@ -30,6 +31,13 @@ export class UaHelper { 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() { this.isLinux = this.checkUseragent('linux'); this.isMacOs = this.checkUseragent('mac'); @@ -39,6 +47,7 @@ export class UaHelper { this.isMobile = this.checkUseragent('mobile'); this.isChrome = this.checkUseragent('chrome'); this.isIOS = this.checkUseragent('ios'); + this.isStandalone = this.isStandaloneMode(); } } diff --git a/packages/frontend/component/src/hooks/index.ts b/packages/frontend/component/src/hooks/index.ts index 93ae56c400..1655abe94f 100644 --- a/packages/frontend/component/src/hooks/index.ts +++ b/packages/frontend/component/src/hooks/index.ts @@ -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'; diff --git a/packages/frontend/component/src/hooks/use-theme-color-meta.ts b/packages/frontend/component/src/hooks/use-theme-color-meta.ts new file mode 100644 index 0000000000..fa1a6aaf2b --- /dev/null +++ b/packages/frontend/component/src/hooks/use-theme-color-meta.ts @@ -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 +) => { + useThemeColorMeta(useThemeValueV1(...args)); +}; + +export const useThemeColorV2 = ( + ...args: Parameters +) => { + useThemeColorMeta(useThemeValueV2(...args)); +}; diff --git a/packages/frontend/component/src/hooks/use-theme-value.ts b/packages/frontend/component/src/hooks/use-theme-value.ts new file mode 100644 index 0000000000..c549a0c6c8 --- /dev/null +++ b/packages/frontend/component/src/hooks/use-theme-value.ts @@ -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) => { + const { resolvedTheme } = useTheme(); + + return resolvedTheme === 'dark' ? darkTheme[key] : lightTheme[key]; +}; diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index 9ba18efae0..2ad4ce1a4d 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -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'; diff --git a/packages/frontend/component/src/ui/menu/mobile/root.tsx b/packages/frontend/component/src/ui/menu/mobile/root.tsx index fbe67dfc8e..bf4b33c837 100644 --- a/packages/frontend/component/src/ui/menu/mobile/root.tsx +++ b/packages/frontend/component/src/ui/menu/mobile/root.tsx @@ -111,10 +111,6 @@ export const MobileMenu = ({ className: clsx(className, styles.mobileMenuModal), ...otherContentOptions, }} - contentWrapperStyle={{ - alignItems: 'end', - paddingBottom: 10, - }} >
( }} {...otherOverlayOptions} > -
( {children} -
+ diff --git a/packages/frontend/component/src/ui/modal/styles.css.ts b/packages/frontend/component/src/ui/modal/styles.css.ts index 78171882eb..7af5448555 100644 --- a/packages/frontend/component/src/ui/modal/styles.css.ts +++ b/packages/frontend/component/src/ui/modal/styles.css.ts @@ -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)', }, }, diff --git a/packages/frontend/component/src/ui/safe-area/index.tsx b/packages/frontend/component/src/ui/safe-area/index.tsx new file mode 100644 index 0000000000..dcf97cf960 --- /dev/null +++ b/packages/frontend/component/src/ui/safe-area/index.tsx @@ -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 { + top?: boolean; + bottom?: boolean; + topOffset?: number | string; + bottomOffset?: number | string; +} + +export const SafeArea = forwardRef( + function SafeArea( + { + children, + className, + style, + top, + bottom, + topOffset = 0, + bottomOffset = 0, + ...attrs + }, + ref + ) { + return ( +
+ {children} +
+ ); + } +); diff --git a/packages/frontend/component/src/ui/safe-area/style.css.ts b/packages/frontend/component/src/ui/safe-area/style.css.ts new file mode 100644 index 0000000000..af217c0f4e --- /dev/null +++ b/packages/frontend/component/src/ui/safe-area/style.css.ts @@ -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})`, + }, + }, +}); diff --git a/packages/frontend/core/public/apple-touch-icon.png b/packages/frontend/core/public/apple-touch-icon.png index af8af0dbf82e7ee6f3675ae0f33a934cb315d122..1251d86c1c7340291165e8f1a542a857e355e898 100644 GIT binary patch literal 19657 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 aHRAu8?y+;7nn~9H0000 { const location = useLiveData(workbench.location$); return ( -
    - {routes.map(route => { - const Link = route.LinkComponent || WorkbenchLink; + +
      + {routes.map(route => { + const Link = route.LinkComponent || WorkbenchLink; - const isActive = route.isActive - ? route.isActive(location) - : location.pathname === route.to; - return ( - -
    • - -
    • - - ); - })} -
    + const isActive = route.isActive + ? route.isActive(location) + : location.pathname === route.to; + return ( + +
  • + +
  • + + ); + })} +
+ ); }; diff --git a/packages/frontend/mobile/src/components/app-tabs/styles.css.ts b/packages/frontend/mobile/src/components/app-tabs/styles.css.ts index c464b73e8f..38d081127c 100644 --- a/packages/frontend/mobile/src/components/app-tabs/styles.css.ts +++ b/packages/frontend/mobile/src/components/app-tabs/styles.css.ts @@ -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', diff --git a/packages/frontend/mobile/src/components/page-header/index.tsx b/packages/frontend/mobile/src/components/page-header/index.tsx index 9cf509887e..9a7383b3dd 100644 --- a/packages/frontend/mobile/src/components/page-header/index.tsx +++ b/packages/frontend/mobile/src/components/page-header/index.tsx @@ -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( +export const PageHeader = forwardRef( function PageHeader( { back, @@ -65,38 +65,41 @@ export const PageHeader = forwardRef( }, [backAction]); return ( -
-
- {back ? ( - } - /> - ) : null} - {prefix} -
+
+
+ {back ? ( + } + /> + ) : null} + {prefix} +
-
- {children} -
+
+ {children} +
-
- {suffix} -
-
+
+ {suffix} +
+
+ ); } ); diff --git a/packages/frontend/mobile/src/components/page-header/styles.css.ts b/packages/frontend/mobile/src/components/page-header/styles.css.ts index 2ea30885ab..6a2f9af065 100644 --- a/packages/frontend/mobile/src/components/page-header/styles.css.ts +++ b/packages/frontend/mobile/src/components/page-header/styles.css.ts @@ -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': { diff --git a/packages/frontend/mobile/src/pages/workspace/all.tsx b/packages/frontend/mobile/src/pages/workspace/all.tsx index 129266e697..8361d37ff9 100644 --- a/packages/frontend/mobile/src/pages/workspace/all.tsx +++ b/packages/frontend/mobile/src/pages/workspace/all.tsx @@ -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 ( <> } /> - + + + ); diff --git a/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx b/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx index 888ec06289..34eccd2ecd 100644 --- a/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx +++ b/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx @@ -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, diff --git a/packages/frontend/mobile/src/pages/workspace/collection/index.tsx b/packages/frontend/mobile/src/pages/workspace/collection/index.tsx index 6d1a83d80c..21e75c971a 100644 --- a/packages/frontend/mobile/src/pages/workspace/collection/index.tsx +++ b/packages/frontend/mobile/src/pages/workspace/collection/index.tsx @@ -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 ( <> diff --git a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx index 01cfb66e05..899df02a8c 100644 --- a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx +++ b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx @@ -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; diff --git a/packages/frontend/mobile/src/pages/workspace/home.tsx b/packages/frontend/mobile/src/pages/workspace/home.tsx index 970a70ffd1..d055b5e35c 100644 --- a/packages/frontend/mobile/src/pages/workspace/home.tsx +++ b/packages/frontend/mobile/src/pages/workspace/home.tsx @@ -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 ( -
- - {runtimeConfig.enableOrganize && } - - - -
+ +
+ + {runtimeConfig.enableOrganize && } + + + +
+
); diff --git a/packages/frontend/mobile/src/pages/workspace/search.tsx b/packages/frontend/mobile/src/pages/workspace/search.tsx index 687aa90a75..f480c7dd82 100644 --- a/packages/frontend/mobile/src/pages/workspace/search.tsx +++ b/packages/frontend/mobile/src/pages/workspace/search.tsx @@ -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 ( <> -
- -
+ +
+ +
+
{searchInput ? : } diff --git a/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx b/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx index e1d0d08f30..a2bfb3c824 100644 --- a/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx +++ b/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx @@ -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; diff --git a/packages/frontend/mobile/src/pages/workspace/tag/index.tsx b/packages/frontend/mobile/src/pages/workspace/tag/index.tsx index 0cfd2487b2..98d7826407 100644 --- a/packages/frontend/mobile/src/pages/workspace/tag/index.tsx +++ b/packages/frontend/mobile/src/pages/workspace/tag/index.tsx @@ -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 ( <> diff --git a/packages/frontend/mobile/src/styles/mobile.css.ts b/packages/frontend/mobile/src/styles/mobile.css.ts index d7f315ed91..0c00a2ea5e 100644 --- a/packages/frontend/mobile/src/styles/mobile.css.ts +++ b/packages/frontend/mobile/src/styles/mobile.css.ts @@ -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', -}); diff --git a/packages/frontend/mobile/src/views/all-docs/header.tsx b/packages/frontend/mobile/src/views/all-docs/header.tsx index 32547d95c5..3e129e15ef 100644 --- a/packages/frontend/mobile/src/views/all-docs/header.tsx +++ b/packages/frontend/mobile/src/views/all-docs/header.tsx @@ -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 ( <> -
- -
- {operations ? ( - - } /> - - ) : null} -
-
-
+ +
+ +
+ {operations ? ( + + } /> + + ) : null} +
+
+
+ +
+ ); }; diff --git a/packages/frontend/mobile/src/views/all-docs/style.css.ts b/packages/frontend/mobile/src/views/all-docs/style.css.ts index b9b33cc71a..90ecc5775c 100644 --- a/packages/frontend/mobile/src/views/all-docs/style.css.ts +++ b/packages/frontend/mobile/src/views/all-docs/style.css.ts @@ -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', diff --git a/packages/frontend/mobile/src/views/home-header/index.tsx b/packages/frontend/mobile/src/views/home-header/index.tsx index b9e6861c51..37b92660ef 100644 --- a/packages/frontend/mobile/src/views/home-header/index.tsx +++ b/packages/frontend/mobile/src/views/home-header/index.tsx @@ -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 (
-
+
@@ -60,8 +64,10 @@ export const HomeHeader = () => {
-
-
+ + +
+
); }; diff --git a/packages/frontend/mobile/src/views/home-header/styles.css.ts b/packages/frontend/mobile/src/views/home-header/styles.css.ts index 19f1c3fb16..c23743ca4c 100644 --- a/packages/frontend/mobile/src/views/home-header/styles.css.ts +++ b/packages/frontend/mobile/src/views/home-header/styles.css.ts @@ -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({ diff --git a/tools/cli/src/webpack/template.html b/tools/cli/src/webpack/template.html index b7e098bcc8..3a80c2874e 100644 --- a/tools/cli/src/webpack/template.html +++ b/tools/cli/src/webpack/template.html @@ -4,8 +4,16 @@ + + + + + AFFiNE diff --git a/tools/cli/src/webpack/webpack.config.ts b/tools/cli/src/webpack/webpack.config.ts index bd7f574564..4ef523fee1 100644 --- a/tools/cli/src/webpack/webpack.config.ts +++ b/tools/cli/src/webpack/webpack.config.ts @@ -74,6 +74,7 @@ export function createWebpackConfig(cwd: string, flags: BuildFlags) { GIT_SHORT_SHA: gitShortHash(), DESCRIPTION, PUBLIC_PATH: config.output?.publicPath, + VIEWPORT_FIT: flags.distribution === 'mobile' ? 'cover' : 'auto', }; }, });