refactor(core): move mobile components to core (#8258)
This commit is contained in:
192 files changed
+157
-165
No files matched your search
+1
-1
@@ -11,7 +11,7 @@ import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
import { appIconMap, appNames } from '../../../../../pages/open-app';
|
||||
import { appIconMap, appNames } from '../../../../../desktop/pages/open-app';
|
||||
import { popupWindow } from '../../../../../utils';
|
||||
import { relatedLinks } from './config';
|
||||
import * as styles from './style.css';
|
||||
|
||||
@@ -19,10 +19,10 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
|
||||
@@ -18,10 +18,8 @@ import {
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
type WorkspaceProfileInfo,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
@@ -173,8 +171,7 @@ const WorkspaceSyncInfo = ({
|
||||
workspaceProfile: WorkspaceProfileInfo;
|
||||
}) => {
|
||||
const syncStatus = useSyncEngineSyncProgress(workspaceMetadata);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const isCloud = currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const isCloud = workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const { paused, pause } = usePauseAnimation();
|
||||
|
||||
// to make sure that animation will play first time
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SignOutModal } from '../components/affine/sign-out-modal';
|
||||
import { SignOutModal } from '../../components/affine/sign-out-modal';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '../../modules/cloud';
|
||||
import { SignIn } from './auth/sign-in';
|
||||
|
||||
export const PageNotFound = ({
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AIUpgradeSuccess } from '../../components/affine/subscription-landing';
|
||||
|
||||
export const Component = () => {
|
||||
return <AIUpgradeSuccess />;
|
||||
};
|
||||
+3
-3
@@ -23,12 +23,12 @@ import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useMutation } from '../../components/hooks/use-mutation';
|
||||
import { useMutation } from '../../../components/hooks/use-mutation';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
import { AuthService, ServerConfigService } from '../../modules/cloud';
|
||||
} from '../../../components/hooks/use-navigate-helper';
|
||||
import { AuthService, ServerConfigService } from '../../../modules/cloud';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
File renamed without changes.
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../modules/cloud';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../modules/cloud';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
File renamed without changes.
+2
-2
@@ -6,11 +6,11 @@ import { useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AuthPanel } from '../../components/affine/auth';
|
||||
import { AuthPanel } from '../../../components/affine/auth';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
} from '../../../components/hooks/use-navigate-helper';
|
||||
|
||||
export const SignIn = () => {
|
||||
const session = useService(AuthService).session;
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../components/hooks/use-navigate-helper';
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
|
||||
export const Component = () => {
|
||||
const t = useI18n();
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useNavigateHelper } from '../components/hooks/use-navigate-helper';
|
||||
import { ImportTemplateDialogService } from '../modules/import-template';
|
||||
import { useNavigateHelper } from '../../components/hooks/use-navigate-helper';
|
||||
import { ImportTemplateDialogService } from '../../modules/import-template';
|
||||
|
||||
export const Component = () => {
|
||||
const importTemplateDialogService = useService(ImportTemplateDialogService);
|
||||
+5
-5
@@ -17,11 +17,11 @@ import { type LoaderFunction, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
buildShowcaseWorkspace,
|
||||
createFirstAppData,
|
||||
} from '../bootstrap/first-app-data';
|
||||
import { AppFallback } from '../components/affine/app-container';
|
||||
import { useNavigateHelper } from '../components/hooks/use-navigate-helper';
|
||||
import { WorkspaceNavigator } from '../components/workspace-selector';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
} from '../../bootstrap/first-app-data';
|
||||
import { AppFallback } from '../../components/affine/app-container';
|
||||
import { useNavigateHelper } from '../../components/hooks/use-navigate-helper';
|
||||
import { WorkspaceNavigator } from '../../components/workspace-selector';
|
||||
import { AuthService } from '../../modules/cloud';
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
return null;
|
||||
+3
-3
@@ -11,12 +11,12 @@ import { useCallback, useEffect } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../components/atoms';
|
||||
import { authAtom } from '../../components/atoms';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '../../modules/cloud';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const inviteId = args.params.inviteId || '';
|
||||
+3
-3
@@ -3,15 +3,15 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
import { Onboarding } from '../components/affine/onboarding/onboarding';
|
||||
import { Onboarding } from '../../components/affine/onboarding/onboarding';
|
||||
import {
|
||||
appConfigStorage,
|
||||
useAppConfigStorage,
|
||||
} from '../components/hooks/use-app-config-storage';
|
||||
} from '../../components/hooks/use-app-config-storage';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../components/hooks/use-navigate-helper';
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
|
||||
export const loader = () => {
|
||||
if (!BUILD_CONFIG.isElectron && !appConfigStorage.get('onBoarding')) {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { AllWorkspaceModals } from '../components/providers/modal-provider';
|
||||
import { AllWorkspaceModals } from '../../components/providers/modal-provider';
|
||||
|
||||
export const RootWrapper = () => {
|
||||
return (
|
||||
File renamed without changes.
+3
-3
@@ -7,12 +7,12 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||
|
||||
import { generateSubscriptionCallbackLink } from '../components/hooks/affine/use-subscription-notify';
|
||||
import { generateSubscriptionCallbackLink } from '../../components/hooks/affine/use-subscription-notify';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../components/hooks/use-navigate-helper';
|
||||
import { AuthService, SubscriptionService } from '../modules/cloud';
|
||||
} from '../../components/hooks/use-navigate-helper';
|
||||
import { AuthService, SubscriptionService } from '../../modules/cloud';
|
||||
import { container } from './subscribe.css';
|
||||
|
||||
export const Component = () => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ThemeEditor } from '../modules/theme-editor';
|
||||
import { ThemeEditor } from '../../modules/theme-editor';
|
||||
|
||||
export const Component = () => {
|
||||
return <ThemeEditor />;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CloudUpgradeSuccess } from '../../components/affine/subscription-landing';
|
||||
|
||||
export const Component = () => {
|
||||
return <CloudUpgradeSuccess />;
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -15,8 +15,8 @@ import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||
import { CollectionService } from '../../../../modules/collection';
|
||||
import { ViewBody, ViewHeader } from '../../../../modules/workbench';
|
||||
import { EmptyCollectionList } from '../page-list-empty';
|
||||
import { AllCollectionHeader } from './header';
|
||||
import * as styles from './index.css';
|
||||
+3
-3
@@ -3,12 +3,12 @@ import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { filterContainerStyle } from '../../../components/filter-container.css';
|
||||
import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper';
|
||||
import { filterContainerStyle } from '../../../../components/filter-container.css';
|
||||
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
|
||||
import {
|
||||
FilterList,
|
||||
SaveAsCollectionButton,
|
||||
} from '../../../components/page-list';
|
||||
} from '../../../../components/page-list';
|
||||
|
||||
export const FilterContainer = ({
|
||||
filters,
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -19,7 +19,7 @@ import {
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../modules/workbench';
|
||||
} from '../../../../modules/workbench';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../modules/workbench';
|
||||
} from '../../../../modules/workbench';
|
||||
import { EmptyTagList } from '../page-list-empty';
|
||||
import * as styles from './all-tag.css';
|
||||
import { AllTagHeader } from './header';
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -19,14 +19,14 @@ import {
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper';
|
||||
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
|
||||
import {
|
||||
useIsActiveView,
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../../modules/workbench';
|
||||
} from '../../../../modules/workbench';
|
||||
import { CollectionDetailHeader } from './header';
|
||||
|
||||
export const CollectionDetail = ({
|
||||
File renamed without changes.
+4
-4
@@ -22,10 +22,10 @@ import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||
import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
||||
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
||||
import { HeaderDivider } from '../../../components/pure/header';
|
||||
import { SharePageButton } from '../../../../components/affine/share-page-modal';
|
||||
import { appSidebarFloatingAtom } from '../../../../components/app-sidebar';
|
||||
import { BlocksuiteHeaderTitle } from '../../../../components/blocksuite/block-suite-header/title/index';
|
||||
import { HeaderDivider } from '../../../../components/pure/header';
|
||||
import * as styles from './detail-page-header.css';
|
||||
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+10
-10
@@ -27,22 +27,22 @@ import clsx from 'clsx';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../../components/hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useActiveBlocksuiteEditor } from '../../../components/hooks/use-block-suite-editor';
|
||||
import { usePageDocumentTitle } from '../../../components/hooks/use-global-state';
|
||||
import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper';
|
||||
import { PageDetailEditor } from '../../../components/page-detail-editor';
|
||||
import { TrashPageFooter } from '../../../components/pure/trash-page-footer';
|
||||
import { TopTip } from '../../../components/top-tip';
|
||||
import { AffineErrorBoundary } from '../../../../components/affine/affine-error-boundary';
|
||||
import { GlobalPageHistoryModal } from '../../../../components/affine/page-history-modal';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../../../components/hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useActiveBlocksuiteEditor } from '../../../../components/hooks/use-block-suite-editor';
|
||||
import { usePageDocumentTitle } from '../../../../components/hooks/use-global-state';
|
||||
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
|
||||
import { PageDetailEditor } from '../../../../components/page-detail-editor';
|
||||
import { TrashPageFooter } from '../../../../components/pure/trash-page-footer';
|
||||
import { TopTip } from '../../../../components/top-tip';
|
||||
import {
|
||||
useIsActiveView,
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewSidebarTab,
|
||||
WorkbenchService,
|
||||
} from '../../../modules/workbench';
|
||||
} from '../../../../modules/workbench';
|
||||
import { PageNotFound } from '../../404';
|
||||
import * as styles from './detail-page.css';
|
||||
import { DetailPageHeader } from './detail-page-header';
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { workspaceRoutes } from '@affine/core/workspace-router';
|
||||
import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
|
||||
import { ZipTransformer } from '@blocksuite/blocks';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
@@ -14,9 +14,9 @@ import type { ReactElement } from 'react';
|
||||
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { matchPath, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceLayout } from '../../components/layouts/workspace-layout';
|
||||
import { WorkbenchRoot } from '../../modules/workbench';
|
||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceLayout } from '../../../components/layouts/workspace-layout';
|
||||
import { WorkbenchRoot } from '../../../modules/workbench';
|
||||
import { PageNotFound } from '../404';
|
||||
import { SharePage } from './share/share-page';
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Component = (): ReactElement => {
|
||||
match.params.docId &&
|
||||
match.params.workspaceId &&
|
||||
// // TODO(eyhn): need a better way to check if it's a docId
|
||||
workspaceRoutes.find(route =>
|
||||
workbenchRoutes.find(route =>
|
||||
matchPath(route.path, '/' + match.params.docId)
|
||||
)?.path === '/:pageId'
|
||||
) {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -20,7 +20,7 @@ import {
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '../../modules/workbench';
|
||||
} from '../../../modules/workbench';
|
||||
import { EmptyPageList } from './page-list-empty';
|
||||
import * as styles from './trash-page.css';
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { NavigateContext } from './components/hooks/use-navigate-helper';
|
||||
import { NavigateContext } from '../components/hooks/use-navigate-helper';
|
||||
import { RootWrapper } from './pages/root';
|
||||
|
||||
export function RootRouter() {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
|
||||
export const workspaceRoutes = [
|
||||
export const workbenchRoutes = [
|
||||
{
|
||||
path: '/all',
|
||||
lazy: () => import('./pages/workspace/all-page/all-page'),
|
||||
@@ -0,0 +1,3 @@
|
||||
# mobile components
|
||||
|
||||
Maintain the smallest possible business components here.
|
||||
@@ -0,0 +1,70 @@
|
||||
import { SafeArea } from '@affine/component';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
WorkbenchService,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { Location } from 'react-router-dom';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface Route {
|
||||
to: string;
|
||||
Icon: React.FC;
|
||||
LinkComponent?: React.FC;
|
||||
isActive?: (location: Location) => boolean;
|
||||
}
|
||||
|
||||
const routes: Route[] = [
|
||||
{
|
||||
to: '/home',
|
||||
Icon: MobileHomeIcon,
|
||||
},
|
||||
{
|
||||
to: '/all',
|
||||
Icon: AllDocsIcon,
|
||||
isActive: location =>
|
||||
location.pathname === '/all' ||
|
||||
location.pathname.startsWith('/collection') ||
|
||||
location.pathname.startsWith('/tag'),
|
||||
},
|
||||
{
|
||||
to: '/search',
|
||||
Icon: SearchIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export const AppTabs = () => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
|
||||
return (
|
||||
<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 style={{ lineHeight: 0 }}>
|
||||
<route.Icon />
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</SafeArea>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
import { globalVars } from '../../styles/mobile.css';
|
||||
|
||||
export const appTabs = style({
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
|
||||
width: '100dvw',
|
||||
|
||||
position: 'fixed',
|
||||
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',
|
||||
justifyContent: 'center',
|
||||
width: 0,
|
||||
flex: 1,
|
||||
height: 36,
|
||||
padding: 3,
|
||||
fontSize: 30,
|
||||
color: cssVarV2('icon/primary'),
|
||||
lineHeight: 0,
|
||||
|
||||
selectors: {
|
||||
'&[data-active="true"]': {
|
||||
color: cssVarV2('button/primary'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { PagePreview } from '@affine/core/components/page-list/page-content-preview';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
type WorkbenchLinkProps,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, type ReactNode } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
import { DocCardTags } from './tag';
|
||||
|
||||
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
||||
meta: {
|
||||
id: DocMeta['id'];
|
||||
title?: ReactNode;
|
||||
} & { [key: string]: any };
|
||||
showTags?: boolean;
|
||||
}
|
||||
|
||||
export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
||||
function DocCard({ showTags = true, meta, className, ...attrs }, ref) {
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const favorited = useLiveData(favAdapter.isFavorite$(meta.id, 'doc'));
|
||||
|
||||
const toggleFavorite = useCatchEventCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
favAdapter.toggle(meta.id, 'doc');
|
||||
},
|
||||
[favAdapter, meta.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkbenchLink
|
||||
to={`/${meta.id}`}
|
||||
ref={ref}
|
||||
className={clsx(styles.card, className)}
|
||||
data-testid="doc-card"
|
||||
{...attrs}
|
||||
>
|
||||
<header className={styles.head} data-testid="doc-card-header">
|
||||
<h3 className={styles.title}>
|
||||
{meta.title || <span className={styles.untitled}>Untitled</span>}
|
||||
</h3>
|
||||
<IconButton
|
||||
aria-label="favorite"
|
||||
icon={
|
||||
<IsFavoriteIcon onClick={toggleFavorite} favorite={favorited} />
|
||||
}
|
||||
/>
|
||||
</header>
|
||||
<main className={styles.content}>
|
||||
<PagePreview
|
||||
docCollection={workspace.docCollection}
|
||||
pageId={meta.id}
|
||||
emptyFallback={<div className={styles.contentEmpty}>Empty</div>}
|
||||
/>
|
||||
</main>
|
||||
{showTags ? <DocCardTags docId={meta.id} rows={2} /> : null}
|
||||
</WorkbenchLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const card = style({
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
border: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
boxShadow: '0px 2px 3px rgba(0,0,0,0.05)',
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
|
||||
color: 'unset',
|
||||
':visited': { color: 'unset' },
|
||||
':hover': { color: 'unset' },
|
||||
':active': { color: 'unset' },
|
||||
});
|
||||
export const head = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
});
|
||||
export const title = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: -0.43,
|
||||
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const untitled = style({
|
||||
opacity: 0.4,
|
||||
});
|
||||
export const content = style({
|
||||
fontSize: 13,
|
||||
lineHeight: '18px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.08,
|
||||
flex: 1,
|
||||
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const contentEmpty = style({
|
||||
opacity: 0.3,
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const tagColorVar = createVar();
|
||||
|
||||
export const tags = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
gap: 4,
|
||||
});
|
||||
|
||||
export const tag = style({
|
||||
padding: '0px 8px',
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/blackBorder')}`,
|
||||
maxWidth: '100%',
|
||||
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
fontWeight: 400,
|
||||
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
|
||||
':before': {
|
||||
content: "''",
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: tagColorVar,
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export const more = style({
|
||||
fontSize: 16,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
|
||||
import * as styles from './tag.css';
|
||||
|
||||
const DocCardTag = ({ tag }: { tag: Tag }) => {
|
||||
const name = useLiveData(tag.value$);
|
||||
const color = useLiveData(tag.color$);
|
||||
|
||||
return (
|
||||
<li
|
||||
data-name={name}
|
||||
data-color={color}
|
||||
className={styles.tag}
|
||||
style={assignInlineVars({ [styles.tagColorVar]: color })}
|
||||
>
|
||||
{name}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const DocCardTagsRenderer = ({ tags }: { tags: Tag[] }) => {
|
||||
return (
|
||||
<ul className={styles.tags}>
|
||||
{tags.slice(0, 2).map(tag => (
|
||||
<DocCardTag key={tag.id} tag={tag} />
|
||||
))}
|
||||
{tags.length > 2 ? <MoreHorizontalIcon className={styles.more} /> : null}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocCardTags = ({ docId }: { docId: string; rows?: number }) => {
|
||||
const tagService = useService(TagService);
|
||||
const tags = useLiveData(tagService.tagList.tagsByPageId$(docId));
|
||||
|
||||
if (!tags.length) return null;
|
||||
return <DocCardTagsRenderer tags={tags} />;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './app-tabs';
|
||||
export * from './doc-card';
|
||||
export * from './page-header';
|
||||
export * from './search-input';
|
||||
export * from './search-result';
|
||||
export * from './user-plan-tag';
|
||||
export * from './workspace-selector';
|
||||
@@ -0,0 +1,105 @@
|
||||
import { IconButton, SafeArea } from '@affine/component';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HtmlHTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface PageHeaderProps
|
||||
extends Omit<HtmlHTMLAttributes<HTMLHeadElement>, 'prefix'> {
|
||||
/**
|
||||
* whether to show back button
|
||||
*/
|
||||
back?: boolean;
|
||||
/**
|
||||
* Override back button action
|
||||
*/
|
||||
backAction?: () => void;
|
||||
|
||||
/**
|
||||
* prefix content, shown after back button(if exists)
|
||||
*/
|
||||
prefix?: ReactNode;
|
||||
|
||||
/**
|
||||
* suffix content
|
||||
*/
|
||||
suffix?: ReactNode;
|
||||
|
||||
/**
|
||||
* Weather to center the content
|
||||
* @default true
|
||||
*/
|
||||
centerContent?: boolean;
|
||||
|
||||
prefixClassName?: string;
|
||||
prefixStyle?: React.CSSProperties;
|
||||
suffixClassName?: string;
|
||||
suffixStyle?: React.CSSProperties;
|
||||
}
|
||||
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
function PageHeader(
|
||||
{
|
||||
back,
|
||||
backAction,
|
||||
prefix,
|
||||
suffix,
|
||||
children,
|
||||
className,
|
||||
centerContent = true,
|
||||
prefixClassName,
|
||||
prefixStyle,
|
||||
suffixClassName,
|
||||
suffixStyle,
|
||||
...attrs
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const handleRouteBack = useCallback(() => {
|
||||
backAction ? backAction() : history.back();
|
||||
}, [backAction]);
|
||||
|
||||
return (
|
||||
<SafeArea
|
||||
top
|
||||
ref={ref}
|
||||
className={clsx(styles.root, className)}
|
||||
data-testid="mobile-page-header"
|
||||
{...attrs}
|
||||
>
|
||||
<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.suffix, suffixClassName)}
|
||||
style={suffixStyle}
|
||||
>
|
||||
{suffix}
|
||||
</section>
|
||||
</header>
|
||||
</SafeArea>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
width: '100%',
|
||||
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': {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 'fit-content',
|
||||
maxWidth: 'calc(100% - 12px - 88px - 16px)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:not(.center)': {
|
||||
width: 0,
|
||||
flex: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const spacer = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
});
|
||||
export const prefix = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0,
|
||||
});
|
||||
export const suffix = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useAutoFocus } from '@affine/component';
|
||||
import { getSvgPath } from '@blocksuite/global/utils';
|
||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
type FormEventHandler,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
export interface SearchInputProps
|
||||
extends Omit<HTMLProps<HTMLInputElement>, 'onInput'> {
|
||||
value?: string;
|
||||
height?: number;
|
||||
cornerRadius?: number;
|
||||
cornerSmoothing?: number;
|
||||
debounce?: number;
|
||||
onInput?: (value: string) => void;
|
||||
}
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
function SearchInput(
|
||||
{
|
||||
className,
|
||||
style,
|
||||
placeholder = 'Search',
|
||||
value = '',
|
||||
height = 44,
|
||||
cornerRadius = 10,
|
||||
cornerSmoothing = 0.6,
|
||||
autoFocus,
|
||||
debounce: debounceDuration,
|
||||
onInput,
|
||||
onClick,
|
||||
...attrs
|
||||
},
|
||||
upstreamRef
|
||||
) {
|
||||
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
const clipPath = useMemo(
|
||||
() => getSvgPath({ width, height, cornerRadius, cornerSmoothing }),
|
||||
[cornerRadius, cornerSmoothing, height, width]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWidth(containerRef.current?.offsetWidth ?? 0);
|
||||
}, []);
|
||||
|
||||
const emitValue = useMemo(() => {
|
||||
const cb = (value: string) => onInput?.(value);
|
||||
return debounceDuration ? debounce(cb, debounceDuration) : cb;
|
||||
}, [debounceDuration, onInput]);
|
||||
|
||||
const handleInput: FormEventHandler<HTMLInputElement> = useCallback(
|
||||
e => {
|
||||
const value = e.currentTarget.value;
|
||||
setInputValue(value);
|
||||
emitValue(value);
|
||||
},
|
||||
[emitValue]
|
||||
);
|
||||
|
||||
const inputRef = (el: HTMLInputElement | null) => {
|
||||
focusRef.current = el;
|
||||
if (upstreamRef) {
|
||||
if (typeof upstreamRef === 'function') {
|
||||
upstreamRef(el);
|
||||
} else {
|
||||
upstreamRef.current = el;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
ref={containerRef}
|
||||
className={clsx(styles.wrapper, className)}
|
||||
style={{ ...style, height, clipPath: `path('${clipPath}')` }}
|
||||
>
|
||||
<div className={styles.prefixIcon}>
|
||||
<SearchIcon width="20" height="20" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
{...attrs}
|
||||
value={inputValue}
|
||||
onInput={handleInput}
|
||||
className={styles.input}
|
||||
/>
|
||||
|
||||
{!inputValue ? (
|
||||
<div className={styles.placeholder}>{placeholder}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { generateIdentifier, style } from '@vanilla-extract/css';
|
||||
|
||||
export const searchVTName = generateIdentifier('mobile-search-input');
|
||||
export const searchVTScope = generateIdentifier('mobile-search');
|
||||
|
||||
export const wrapper = style({
|
||||
position: 'relative',
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
|
||||
selectors: {
|
||||
[`[data-${searchVTScope}] &`]: {
|
||||
viewTransitionName: searchVTName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const prefixIcon = style({
|
||||
position: 'absolute',
|
||||
width: 36,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVarV2('icon/primary'),
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
padding: '11px 8px 11px 36px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
|
||||
fontWeight: 400,
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const placeholder = style([
|
||||
input,
|
||||
{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
pointerEvents: 'none',
|
||||
color: cssVarV2('text/secondary'),
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './search-res-label';
|
||||
export * from './universal-item';
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { QuickSearchItem } from '@affine/core/modules/quicksearch';
|
||||
import { HighlightText } from '@affine/core/modules/quicksearch/views/highlight-text';
|
||||
import { isI18nString, useI18n } from '@affine/i18n';
|
||||
|
||||
export interface SearchResLabelProps {
|
||||
item: QuickSearchItem;
|
||||
}
|
||||
export const SearchResLabel = ({ item }: SearchResLabelProps) => {
|
||||
const i18n = useI18n();
|
||||
|
||||
const text = !isI18nString(item.label)
|
||||
? i18n.t(item.label.title)
|
||||
: i18n.t(item.label);
|
||||
return <HighlightText text={text} start="<b>" end="</b>" />;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const item = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
|
||||
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
height: 44,
|
||||
|
||||
color: 'unset',
|
||||
':visited': { color: 'unset' },
|
||||
':hover': { color: 'unset' },
|
||||
':active': { color: 'unset' },
|
||||
':focus': { color: 'unset' },
|
||||
});
|
||||
export const iconWrapper = style({
|
||||
width: 32,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
fontSize: 24,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
export const content = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.43,
|
||||
color: cssVarV2('text/primary'),
|
||||
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const suffixIcon = style({
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { QuickSearchItem } from '@affine/core/modules/quicksearch';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { SearchResLabel } from './search-res-label';
|
||||
import * as styles from './universal-item.css';
|
||||
|
||||
export interface UniversalSearchResultItemProps {
|
||||
id: string;
|
||||
item: QuickSearchItem;
|
||||
category: 'tag' | 'collection';
|
||||
}
|
||||
export const UniversalSearchResultItem = ({
|
||||
id,
|
||||
item,
|
||||
category,
|
||||
}: UniversalSearchResultItemProps) => {
|
||||
return (
|
||||
<WorkbenchLink to={`/${category}/${id}`} className={styles.item}>
|
||||
<div className={styles.iconWrapper}>
|
||||
{item.icon &&
|
||||
(typeof item.icon === 'function' ? <item.icon /> : item.icon)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<SearchResLabel item={item} />
|
||||
</div>
|
||||
|
||||
<ArrowRightSmallIcon fontSize="16px" className={styles.suffixIcon} />
|
||||
</WorkbenchLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, type HTMLProps, useEffect } from 'react';
|
||||
|
||||
import { tag } from './style.css';
|
||||
|
||||
export const UserPlanTag = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLProps<HTMLDivElement>
|
||||
>(function UserPlanTag({ className, ...attrs }, ref) {
|
||||
const { serverConfigService, subscriptionService } = useServices({
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
});
|
||||
const hasPayment = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(r => r?.payment)
|
||||
);
|
||||
const plan = useLiveData(
|
||||
subscriptionService.subscription.pro$.map(subscription =>
|
||||
subscription !== null ? subscription?.plan : null
|
||||
)
|
||||
);
|
||||
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
|
||||
const isLoading = plan === null;
|
||||
|
||||
useEffect(() => {
|
||||
// revalidate subscription to get the latest status
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
if (!hasPayment) return null;
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
const planLabel = isBeliever ? 'Believer' : (plan ?? SubscriptionPlan.Free);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(tag, className)}
|
||||
data-is-believer={isBeliever}
|
||||
{...attrs}
|
||||
>
|
||||
{planLabel}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const tag = style({
|
||||
display: 'flex',
|
||||
fontSize: cssVar('fontXs'),
|
||||
height: 20,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
color: cssVar('pureWhite'),
|
||||
backgroundColor: cssVar('brandColor'),
|
||||
padding: '0 4px',
|
||||
borderRadius: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
selectors: {
|
||||
'&[data-is-believer="true"]': {
|
||||
backgroundColor: '#374151',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const card = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
});
|
||||
|
||||
export const label = style({
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
color: cssVarV2('text/primary'),
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const dropdownIcon = style({
|
||||
fontSize: 24,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, type HTMLAttributes } from 'react';
|
||||
|
||||
import { card, dropdownIcon, label } from './card.css';
|
||||
|
||||
export interface CurrentWorkspaceCardProps
|
||||
extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const CurrentWorkspaceCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
CurrentWorkspaceCardProps
|
||||
>(function CurrentWorkspaceCard({ onClick, className, ...attrs }, ref) {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const info = useWorkspaceInfo(currentWorkspace.meta);
|
||||
const name = info?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className={clsx(card, className)}
|
||||
{...attrs}
|
||||
>
|
||||
<WorkspaceAvatar
|
||||
key={currentWorkspace.id}
|
||||
meta={currentWorkspace.meta}
|
||||
rounded={3}
|
||||
data-testid="workspace-avatar"
|
||||
size={40}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
/>
|
||||
<div className={label}>
|
||||
{name}
|
||||
<ArrowDownSmallIcon className={dropdownIcon} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MobileMenu } from '@affine/component';
|
||||
import { track } from '@affine/track';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { CurrentWorkspaceCard } from './current-card';
|
||||
import { SelectorMenu } from './menu';
|
||||
|
||||
export const WorkspaceSelector = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
|
||||
const openMenu = useCallback(() => {
|
||||
track.$.navigationPanel.workspaceList.open();
|
||||
setOpen(true);
|
||||
}, []);
|
||||
const close = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
// revalidate workspace list when open workspace list
|
||||
useEffect(() => {
|
||||
if (open) workspaceManager.list.revalidate();
|
||||
}, [workspaceManager, open]);
|
||||
|
||||
return (
|
||||
<MobileMenu
|
||||
items={<SelectorMenu onClose={close} />}
|
||||
rootOptions={{ open }}
|
||||
contentOptions={{
|
||||
onInteractOutside: close,
|
||||
onEscapeKeyDown: close,
|
||||
style: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<CurrentWorkspaceCard onClick={openMenu} />
|
||||
</MobileMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
maxHeight: 'calc(100dvh - 100px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const divider = style({
|
||||
height: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
':before': {
|
||||
content: '""',
|
||||
width: '100%',
|
||||
height: 0.5,
|
||||
background: cssVar('dividerColor'),
|
||||
},
|
||||
});
|
||||
|
||||
export const head = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
padding: '10px 16px',
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
color: cssVarV2('text/primary'),
|
||||
});
|
||||
export const body = style({
|
||||
overflowY: 'auto',
|
||||
flexShrink: 0,
|
||||
flex: 1,
|
||||
});
|
||||
export const wsList = style({});
|
||||
export const wsListTitle = style({
|
||||
padding: '6px 16px',
|
||||
fontSize: 13,
|
||||
lineHeight: '18px',
|
||||
letterSpacing: -0.08,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const wsItem = style({
|
||||
padding: '4px 12px',
|
||||
});
|
||||
export const wsCard = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
width: '100%',
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
gap: 8,
|
||||
|
||||
':active': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
});
|
||||
export const wsName = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
textAlign: 'left',
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { type HTMLAttributes, useCallback, useMemo } from 'react';
|
||||
|
||||
import * as styles from './menu.css';
|
||||
|
||||
const filterByFlavour = (
|
||||
workspaces: WorkspaceMetadata[],
|
||||
flavour: WorkspaceFlavour
|
||||
) => workspaces.filter(ws => flavour === ws.flavour);
|
||||
|
||||
const WorkspaceItem = ({
|
||||
workspace,
|
||||
className,
|
||||
...attrs
|
||||
}: { workspace: WorkspaceMetadata } & HTMLAttributes<HTMLButtonElement>) => {
|
||||
const info = useWorkspaceInfo(workspace);
|
||||
const name = info?.name;
|
||||
const isOwner = info?.isOwner;
|
||||
|
||||
return (
|
||||
<li className={styles.wsItem}>
|
||||
<button className={clsx(styles.wsCard, className)} {...attrs}>
|
||||
<WorkspaceAvatar
|
||||
key={workspace.id}
|
||||
meta={workspace}
|
||||
rounded={6}
|
||||
data-testid="workspace-avatar"
|
||||
size={32}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
/>
|
||||
<div className={styles.wsName}>{name}</div>
|
||||
{!isOwner ? <CollaborationIcon fontSize={24} /> : null}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceList = ({
|
||||
list,
|
||||
title,
|
||||
onClose,
|
||||
}: {
|
||||
title: string;
|
||||
list: WorkspaceMetadata[];
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const toggleWorkspace = useCallback(
|
||||
(id: string) => {
|
||||
if (id !== currentWorkspace.id) {
|
||||
jumpToPage(id, 'all');
|
||||
}
|
||||
onClose?.();
|
||||
},
|
||||
[currentWorkspace.id, jumpToPage, onClose]
|
||||
);
|
||||
|
||||
if (!list.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.wsListTitle}>{title}</section>
|
||||
<ul className={styles.wsList}>
|
||||
{list.map(ws => (
|
||||
<WorkspaceItem
|
||||
key={ws.id}
|
||||
workspace={ws}
|
||||
onClick={() => toggleWorkspace?.(ws.id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
|
||||
const cloudWorkspaces = useMemo(
|
||||
() => filterByFlavour(workspaces, WorkspaceFlavour.AFFINE_CLOUD),
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const localWorkspaces = useMemo(
|
||||
() => filterByFlavour(workspaces, WorkspaceFlavour.LOCAL),
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<header className={styles.head}>
|
||||
Workspace
|
||||
<IconButton onClick={onClose} size="24" icon={<CloseIcon />} />
|
||||
</header>
|
||||
<div className={styles.divider} />
|
||||
<main className={styles.body}>
|
||||
<WorkspaceList
|
||||
onClose={onClose}
|
||||
title="Cloud Sync"
|
||||
list={cloudWorkspaces}
|
||||
/>
|
||||
<div className={styles.divider} />
|
||||
<WorkspaceList
|
||||
onClose={onClose}
|
||||
title="Local Storage"
|
||||
list={localWorkspaces}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type Handler<T extends Event> = (event: T) => void;
|
||||
|
||||
const _handlesMap = new Map<
|
||||
keyof WindowEventMap,
|
||||
Array<Handler<WindowEventMap[keyof WindowEventMap]>>
|
||||
>();
|
||||
|
||||
function initGlobalEvent<T extends keyof WindowEventMap>(name: T) {
|
||||
const prev = _handlesMap.get(name);
|
||||
if (!prev) {
|
||||
const handlers = [] as Handler<WindowEventMap[T]>[];
|
||||
window.addEventListener(name, e => {
|
||||
handlers.forEach(handler => {
|
||||
handler(e);
|
||||
});
|
||||
});
|
||||
_handlesMap.set(name, handlers as any);
|
||||
return handlers;
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
function addListener<T extends keyof WindowEventMap>(
|
||||
name: T,
|
||||
handler: (e: WindowEventMap[T]) => void
|
||||
) {
|
||||
initGlobalEvent(name).push(handler);
|
||||
}
|
||||
|
||||
function removeListener<T extends keyof WindowEventMap>(
|
||||
name: T,
|
||||
handler: Handler<WindowEventMap[T]>
|
||||
) {
|
||||
const handlers = _handlesMap.get(name) as Handler<WindowEventMap[T]>[];
|
||||
const idx = handlers.indexOf(handler);
|
||||
if (idx !== -1) {
|
||||
handlers.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export const useGlobalEvent = <T extends keyof WindowEventMap>(
|
||||
name: T,
|
||||
handler: (e: WindowEventMap[T]) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
addListener(name, handler);
|
||||
|
||||
return () => {
|
||||
removeListener(name, handler);
|
||||
};
|
||||
}, [handler, name]);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { configureMobileSearchModule } from './search';
|
||||
|
||||
export function configureMobileModules(framework: Framework) {
|
||||
configureMobileSearchModule(framework);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type Framework, WorkspaceScope } from '@toeverything/infra';
|
||||
|
||||
import { MobileSearchService } from './service/search';
|
||||
|
||||
export { MobileSearchService };
|
||||
|
||||
export function configureMobileSearchModule(framework: Framework) {
|
||||
framework.scope(WorkspaceScope).service(MobileSearchService);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
CollectionsQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
RecentDocsQuickSearchSession,
|
||||
TagsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
export class MobileSearchService extends Service {
|
||||
readonly recentDocs = this.framework.createEntity(
|
||||
RecentDocsQuickSearchSession
|
||||
);
|
||||
readonly collections = this.framework.createEntity(
|
||||
CollectionsQuickSearchSession
|
||||
);
|
||||
readonly docs = this.framework.createEntity(DocsQuickSearchSession);
|
||||
readonly tags = this.framework.createEntity(TagsQuickSearchSession);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/404</div>;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const Component = () => {
|
||||
return <div>/auth/*</div>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Component as IndexComponent } from '@affine/core/desktop/pages/index';
|
||||
|
||||
// Default route fallback for mobile
|
||||
|
||||
export const Component = () => {
|
||||
// TODO: replace with a mobile version
|
||||
return <IndexComponent defaultIndexRoute={'home'} />;
|
||||
};
|
||||
Loaded 100 of 192 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user