diff --git a/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx b/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx index 4086031fe..eebefc363 100644 --- a/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx +++ b/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx @@ -18,6 +18,31 @@ type GroupOption = { label: string; }; +export function getGroupOptions(t: ReturnType) { + return [ + { + value: 'createDate', + label: t['Created'](), + }, + { + value: 'updatedDate', + label: t['Updated'](), + }, + { + value: 'tag', + label: t['com.affine.page.display.grouping.group-by-tag'](), + }, + { + value: 'favourites', + label: t['com.affine.page.display.grouping.group-by-favourites'](), + }, + { + value: 'none', + label: t['com.affine.page.display.grouping.no-grouping'](), + }, + ] satisfies GroupOption[]; +} + export const PageDisplayMenu = () => { const t = useI18n(); const [workspaceProperties, setProperties] = useAllDocDisplayProperties(); @@ -66,28 +91,7 @@ export const PageDisplayMenu = () => { }, [handleSetDocDisplayProperties, t]); const items = useMemo(() => { - const groupOptions: GroupOption[] = [ - { - value: 'createDate', - label: t['Created'](), - }, - { - value: 'updatedDate', - label: t['Updated'](), - }, - { - value: 'tag', - label: t['com.affine.page.display.grouping.group-by-tag'](), - }, - { - value: 'favourites', - label: t['com.affine.page.display.grouping.group-by-favourites'](), - }, - { - value: 'none', - label: t['com.affine.page.display.grouping.no-grouping'](), - }, - ]; + const groupOptions: GroupOption[] = getGroupOptions(t); const subItems = groupOptions.map(option => ( { let count = MAX_SEARCH_BLOCK_COUNT; while (queue.length && previewLenNeeded > 0 && count-- > 0) { const block = queue.shift(); + // if preview length is enough, skip the rest of the blocks + if (preview.join(' ').trim().length >= MAX_PREVIEW_LENGTH) { + break; + } if (!block) { console.error('Unexpected empty block'); break; @@ -51,7 +55,7 @@ export const getPagePreviewText = (page: Doc) => { } } } - return preview.join(' ').slice(0, MAX_PREVIEW_LENGTH); + return preview.join(' ').trim().slice(0, MAX_PREVIEW_LENGTH); }; const emptyAtom = atom(''); diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx index c00f482db..0ed1c7a5a 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx @@ -14,11 +14,12 @@ export type WorkbenchLinkProps = React.PropsWithChildren< { to: To; onClick?: (e: MouseEvent) => void; + replaceHistory?: boolean; } & React.HTMLProps >; export const WorkbenchLink = forwardRef( - function WorkbenchLink({ to, onClick, ...other }, ref) { + function WorkbenchLink({ to, onClick, replaceHistory, ...other }, ref) { const { featureFlagService, workbenchService } = useServices({ FeatureFlagService, WorkbenchService, @@ -45,10 +46,10 @@ export const WorkbenchLink = forwardRef( } return 'active'; })(); - workbench.open(to, { at }); + workbench.open(to, { at, replaceHistory }); event.preventDefault(); }, - [enableMultiView, onClick, to, workbench] + [enableMultiView, onClick, replaceHistory, to, workbench] ); // eslint suspicious runtime error diff --git a/packages/frontend/core/src/pages/workspace/collection/index.tsx b/packages/frontend/core/src/pages/workspace/collection/index.tsx index e02ea117d..7f5c73dd0 100644 --- a/packages/frontend/core/src/pages/workspace/collection/index.tsx +++ b/packages/frontend/core/src/pages/workspace/collection/index.tsx @@ -121,7 +121,7 @@ export const Component = function CollectionPage() { if (!collection) { return null; } - const inner = isEmpty(collection) ? ( + const inner = isEmptyCollection(collection) ? ( ) : ( @@ -346,7 +346,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => { ); }; -const isEmpty = (collection: Collection) => { +export const isEmptyCollection = (collection: Collection) => { return ( collection.allowList.length === 0 && collection.filterList.length === 0 ); diff --git a/packages/frontend/mobile/src/components/app-tabs/index.tsx b/packages/frontend/mobile/src/components/app-tabs/index.tsx index 9d3077d50..cef9cbed5 100644 --- a/packages/frontend/mobile/src/components/app-tabs/index.tsx +++ b/packages/frontend/mobile/src/components/app-tabs/index.tsx @@ -4,6 +4,7 @@ import { } from '@affine/core/modules/workbench'; import { AllDocsIcon, SearchIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; +import type { Location } from 'react-router-dom'; import { HomeIcon } from './home-icon'; import * as styles from './styles.css'; @@ -12,6 +13,7 @@ interface Route { to: string; Icon: React.FC; LinkComponent?: React.FC; + isActive?: (location: Location) => boolean; } const routes: Route[] = [ @@ -22,6 +24,10 @@ const routes: Route[] = [ { to: '/all', Icon: AllDocsIcon, + isActive: location => + location.pathname === '/all' || + location.pathname.startsWith('/collection') || + location.pathname.startsWith('/tag'), }, { to: '/search', @@ -37,9 +43,13 @@ export const AppTabs = () => {
    {routes.map(route => { const Link = route.LinkComponent || WorkbenchLink; + + const isActive = route.isActive + ? route.isActive(location) + : location.pathname === route.to; return ( { meta: DocMeta; + showTags?: boolean; } export const DocCard = forwardRef( - function DocCard({ meta, className, ...attrs }, ref) { + function DocCard({ showTags = true, meta, className, ...attrs }, ref) { const favAdapter = useService(CompatibleFavoriteItemsAdapter); const workspace = useService(WorkspaceService).workspace; @@ -54,7 +55,7 @@ export const DocCard = forwardRef( emptyFallback={
    Empty
    } /> - + {showTags ? : null} ); } diff --git a/packages/frontend/mobile/src/components/doc-card/tag.tsx b/packages/frontend/mobile/src/components/doc-card/tag.tsx index f2f63f434..faf06a69c 100644 --- a/packages/frontend/mobile/src/components/doc-card/tag.tsx +++ b/packages/frontend/mobile/src/components/doc-card/tag.tsx @@ -122,7 +122,7 @@ const DocCardTagsRenderer = ({ tags, rows }: { tags: Tag[]; rows: number }) => { {tags.map(tag => ( ))} - {/* TODO: more icon */} + {/* TODO(@CatsJuice): more icon */} {/* */}
); diff --git a/packages/frontend/mobile/src/components/index.ts b/packages/frontend/mobile/src/components/index.ts index 8b4fd75f6..dfd0929aa 100644 --- a/packages/frontend/mobile/src/components/index.ts +++ b/packages/frontend/mobile/src/components/index.ts @@ -1,4 +1,5 @@ export * from './app-tabs'; +export * from './doc-card'; export * from './page-header'; export * from './search-button'; export * from './workspace-selector'; 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 0970150c0..50b998cb7 100644 --- a/packages/frontend/mobile/src/components/page-header/styles.css.ts +++ b/packages/frontend/mobile/src/components/page-header/styles.css.ts @@ -1,13 +1,19 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; 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: 'relative', + + position: 'sticky', + top: 0, + zIndex: 1, + backgroundColor: cssVarV2('layer/background/secondary'), }); export const content = style({ selectors: { @@ -15,6 +21,11 @@ export const content = style({ 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, diff --git a/packages/frontend/mobile/src/pages/workspace/all.tsx b/packages/frontend/mobile/src/pages/workspace/all.tsx index 3097c003b..129266e69 100644 --- a/packages/frontend/mobile/src/pages/workspace/all.tsx +++ b/packages/frontend/mobile/src/pages/workspace/all.tsx @@ -1,9 +1,11 @@ import { AppTabs } from '../../components'; +import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views'; export const Component = () => { return ( <> - `workspace/all.tsx`; + } /> + ); diff --git a/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx b/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx index a098207e7..888ec0628 100644 --- a/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx +++ b/packages/frontend/mobile/src/pages/workspace/collection/detail.tsx @@ -1,3 +1,82 @@ +import { notify } 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'; +import { WorkspaceSubPath } from '@affine/core/shared'; +import { + GlobalContextService, + useLiveData, + useServices, + WorkspaceService, +} from '@toeverything/infra'; +import { useCallback, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { AppTabs } from '../../../components'; +import { CollectionDetail, EmptyCollection } from '../../../views'; + export const Component = () => { - return
/workspace/:workspaceId/collection/:collectionId
; + const { collectionService, globalContextService, workspaceService } = + useServices({ + WorkspaceService, + CollectionService, + GlobalContextService, + }); + + const globalContext = globalContextService.globalContext; + const collections = useLiveData(collectionService.collections$); + const params = useParams(); + const navigate = useNavigateHelper(); + const workspace = workspaceService.workspace; + const collection = collections.find(v => v.id === params.collectionId); + + useEffect(() => { + if (collection) { + globalContext.collectionId.set(collection.id); + globalContext.isCollection.set(true); + + return () => { + globalContext.collectionId.set(null); + globalContext.isCollection.set(false); + }; + } + return; + }, [collection, globalContext]); + + const notifyCollectionDeleted = useCallback(() => { + navigate.jumpToSubPath(workspace.id, WorkspaceSubPath.HOME); + const collection = collectionService.collectionsTrash$.value.find( + v => v.collection.id === params.collectionId + ); + let text = 'Collection does not exist'; + if (collection) { + if (collection.userId) { + text = `${collection.collection.name} has been deleted by ${collection.userName}`; + } else { + text = `${collection.collection.name} has been deleted`; + } + } + return notify.error({ title: text }); + }, [collectionService, navigate, params.collectionId, workspace.id]); + + useEffect(() => { + if (!collection) { + notifyCollectionDeleted(); + } + }, [collection, notifyCollectionDeleted]); + + if (!collection) { + return null; + } + + if (isEmptyCollection(collection)) { + return ; + } + + return ( + <> + + + + ); }; diff --git a/packages/frontend/mobile/src/pages/workspace/collection/index.tsx b/packages/frontend/mobile/src/pages/workspace/collection/index.tsx index bdc53f76d..6d1a83d80 100644 --- a/packages/frontend/mobile/src/pages/workspace/collection/index.tsx +++ b/packages/frontend/mobile/src/pages/workspace/collection/index.tsx @@ -1,3 +1,12 @@ +import { AppTabs } from '../../../components'; +import { AllDocsHeader, CollectionList } from '../../../views'; + export const Component = () => { - return
/workspace/:workspaceId/collection
; + return ( + <> + + + + + ); }; diff --git a/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx b/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx index 2aa1bda3a..e1d0d08f3 100644 --- a/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx +++ b/packages/frontend/mobile/src/pages/workspace/tag/detail.tsx @@ -1,3 +1,40 @@ +import { TagService } from '@affine/core/modules/tag'; +import { PageNotFound } from '@affine/core/pages/404'; +import { + GlobalContextService, + useLiveData, + useService, +} from '@toeverything/infra'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { TagDetail } from '../../../views'; + export const Component = () => { - return
/workspace/:workspaceId/tag/:tagId
; + const params = useParams(); + const tagId = params.tagId; + + const globalContext = useService(GlobalContextService).globalContext; + + const tagList = useService(TagService).tagList; + const currentTag = useLiveData(tagList.tagByTagId$(tagId)); + + useEffect(() => { + if (currentTag) { + globalContext.tagId.set(currentTag.id); + globalContext.isTag.set(true); + + return () => { + globalContext.tagId.set(null); + globalContext.isTag.set(false); + }; + } + return; + }, [currentTag, globalContext]); + + if (!currentTag) { + return ; + } + + return ; }; diff --git a/packages/frontend/mobile/src/pages/workspace/tag/index.tsx b/packages/frontend/mobile/src/pages/workspace/tag/index.tsx index 9433819f8..0cfd2487b 100644 --- a/packages/frontend/mobile/src/pages/workspace/tag/index.tsx +++ b/packages/frontend/mobile/src/pages/workspace/tag/index.tsx @@ -1,3 +1,12 @@ +import { AppTabs } from '../../../components'; +import { AllDocsHeader, TagList } from '../../../views'; + export const Component = () => { - return
/workspace/:workspaceId/tag
; + return ( + <> + + + + + ); }; diff --git a/packages/frontend/mobile/src/router.tsx b/packages/frontend/mobile/src/router.tsx index 02f8a7f0d..fd48bc8a8 100644 --- a/packages/frontend/mobile/src/router.tsx +++ b/packages/frontend/mobile/src/router.tsx @@ -7,8 +7,12 @@ import { } from 'react-router-dom'; import { Component as All } from './pages/workspace/all'; +import { Component as Collection } from './pages/workspace/collection'; +import { Component as CollectionDetail } from './pages/workspace/collection/detail'; import { Component as Home } from './pages/workspace/home'; import { Component as Search } from './pages/workspace/search'; +import { Component as Tag } from './pages/workspace/tag'; +import { Component as TagDetail } from './pages/workspace/tag/detail'; export const topLevelRoutes = [ { @@ -67,19 +71,23 @@ export const viewRoutes = [ }, { path: '/collection', - lazy: () => import('./pages/workspace/collection/index'), + // lazy: () => import('./pages/workspace/collection/index'), + Component: Collection, }, { path: '/collection/:collectionId', - lazy: () => import('./pages/workspace/collection/detail'), + // lazy: () => import('./pages/workspace/collection/detail'), + Component: CollectionDetail, }, { path: '/tag', - lazy: () => import('./pages/workspace/tag/index'), + // lazy: () => import('./pages/workspace/tag/index'), + Component: Tag, }, { path: '/tag/:tagId', - lazy: () => import('./pages/workspace/tag/detail'), + // lazy: () => import('./pages/workspace/tag/detail'), + Component: TagDetail, }, { path: '/trash', diff --git a/packages/frontend/mobile/src/views/all-docs/collection/detail.css.ts b/packages/frontend/mobile/src/views/all-docs/collection/detail.css.ts new file mode 100644 index 000000000..cbb2f6fe3 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/detail.css.ts @@ -0,0 +1,19 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const headerContent = style({ + display: 'flex', + alignItems: 'center', + gap: 8, + + fontSize: 17, + fontWeight: 600, + lineHeight: '22px', + letterSpacing: -0.43, + color: cssVarV2('text/primary'), +}); + +export const headerIcon = style({ + fontSize: 24, + color: cssVarV2('icon/primary'), +}); diff --git a/packages/frontend/mobile/src/views/all-docs/collection/detail.tsx b/packages/frontend/mobile/src/views/all-docs/collection/detail.tsx new file mode 100644 index 000000000..0466a58a6 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/detail.tsx @@ -0,0 +1,43 @@ +import { IconButton, MobileMenu } from '@affine/component'; +import type { Collection } from '@affine/env/filter'; +import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc'; + +import { PageHeader } from '../../../components'; +import { AllDocList } from '../doc/list'; +import { AllDocsMenu } from '../doc/menu'; +import * as styles from './detail.css'; + +export const DetailHeader = ({ collection }: { collection: Collection }) => { + return ( + }> + } + /> + + } + > +
+ + {collection.name} +
+
+ ); +}; + +export const CollectionDetail = ({ + collection, +}: { + collection: Collection; +}) => { + return ( + <> + + + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/collection/empty.tsx b/packages/frontend/mobile/src/views/all-docs/collection/empty.tsx new file mode 100644 index 000000000..a4d66dc20 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/empty.tsx @@ -0,0 +1,12 @@ +import type { Collection } from '@affine/env/filter'; + +import { DetailHeader } from './detail'; + +export const EmptyCollection = ({ collection }: { collection: Collection }) => { + return ( + <> + + Empty + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/collection/index.ts b/packages/frontend/mobile/src/views/all-docs/collection/index.ts new file mode 100644 index 000000000..ebf7fe6b2 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/index.ts @@ -0,0 +1,3 @@ +export { CollectionDetail } from './detail'; +export * from './empty'; +export * from './list'; diff --git a/packages/frontend/mobile/src/views/all-docs/collection/item.tsx b/packages/frontend/mobile/src/views/all-docs/collection/item.tsx new file mode 100644 index 000000000..9fe31a420 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/item.tsx @@ -0,0 +1,38 @@ +import { IconButton } from '@affine/component'; +import type { CollectionMeta } from '@affine/core/components/page-list'; +import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; +import { WorkbenchLink } from '@affine/core/modules/workbench'; +import { ViewLayersIcon } from '@blocksuite/icons/rc'; +import { useLiveData, useService } from '@toeverything/infra'; +import { type MouseEvent, useCallback } from 'react'; + +import { item, name, prefixIcon, suffixIcon } from './styles.css'; + +export const CollectionListItem = ({ meta }: { meta: CollectionMeta }) => { + const favAdapter = useService(CompatibleFavoriteItemsAdapter); + + const isFavorite = useLiveData(favAdapter.isFavorite$(meta.id, 'collection')); + + const toggle = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + favAdapter.toggle(meta.id, 'collection'); + }, + [favAdapter, meta.id] + ); + + return ( + + + {meta.title} + } + size="24" + /> + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/collection/list.tsx b/packages/frontend/mobile/src/views/all-docs/collection/list.tsx new file mode 100644 index 000000000..60aa8851e --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/list.tsx @@ -0,0 +1,29 @@ +import type { CollectionMeta } from '@affine/core/components/page-list'; +import { CollectionService } from '@affine/core/modules/collection'; +import { useLiveData, useService } from '@toeverything/infra'; +import { useMemo } from 'react'; + +import { CollectionListItem } from './item'; +import { list } from './styles.css'; + +export const CollectionList = () => { + const collectionService = useService(CollectionService); + const collections = useLiveData(collectionService.collections$); + + const collectionMetas = useMemo( + () => + collections.map( + collection => + ({ ...collection, title: collection.name }) satisfies CollectionMeta + ), + [collections] + ); + + return ( +
    + {collectionMetas.map(meta => ( + + ))} +
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/collection/styles.css.ts b/packages/frontend/mobile/src/views/all-docs/collection/styles.css.ts new file mode 100644 index 000000000..8f17f746d --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/collection/styles.css.ts @@ -0,0 +1,45 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const list = style({}); +export const item = style({ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: 16, + + color: 'unset', + ':visited': { color: 'unset' }, + ':hover': { color: 'unset' }, + ':active': { color: 'unset' }, + ':focus': { color: 'unset' }, + + selectors: { + '&:not(:last-child)': { + borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`, + }, + }, +}); +export const icon = style({ + width: 24, + height: 24, + color: cssVarV2('icon/primary'), + flexShrink: 0, +}); +export const prefixIcon = style([icon]); +export const name = style({ + width: 0, + flex: 1, + + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + + color: cssVarV2('text/primary'), + fontSize: 17, + fontWeight: 600, + lineHeight: '24px', + letterSpacing: -0.43, +}); + +export const suffixIcon = style([icon]); diff --git a/packages/frontend/mobile/src/views/all-docs/doc/index.ts b/packages/frontend/mobile/src/views/all-docs/doc/index.ts new file mode 100644 index 000000000..9c183702d --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/index.ts @@ -0,0 +1,3 @@ +export * from './list'; +export * from './masonry'; +export * from './menu'; diff --git a/packages/frontend/mobile/src/views/all-docs/doc/list.css.ts b/packages/frontend/mobile/src/views/all-docs/doc/list.css.ts new file mode 100644 index 000000000..2f5c6e00c --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/list.css.ts @@ -0,0 +1,32 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { globalStyle, style } from '@vanilla-extract/css'; + +export const groupTitle = style({ + display: 'flex', + alignItems: 'center', + padding: '0px 16px', + width: '100%', +}); +// to override style defined in `core` +globalStyle(`${groupTitle} > div`, { + marginRight: -4, +}); +globalStyle(`${groupTitle} div[data-testid^='group-label']`, { + fontSize: `20px !important`, + color: `${cssVarV2('text/primary')} !important`, + lineHeight: '25px !important', +}); +export const groupTitleIcon = style({ + color: cssVarV2('icon/tertiary'), + transition: 'transform 0.2s', + selectors: { + '[data-state="closed"] &': { + transform: `rotate(-90deg)`, + }, + }, +}); +export const groups = style({ + display: 'flex', + flexDirection: 'column', + gap: 32, +}); diff --git a/packages/frontend/mobile/src/views/all-docs/doc/list.tsx b/packages/frontend/mobile/src/views/all-docs/doc/list.tsx new file mode 100644 index 000000000..7e9aa63b7 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/list.tsx @@ -0,0 +1,88 @@ +import { + type ItemGroupDefinition, + type ItemGroupProps, + useAllDocDisplayProperties, + useFilteredPageMetas, + usePageItemGroupDefinitions, +} from '@affine/core/components/page-list'; +import { itemsToItemGroups } from '@affine/core/components/page-list/items-to-item-group'; +import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; +import type { Tag } from '@affine/core/modules/tag'; +import type { Collection, Filter } from '@affine/env/filter'; +import { ToggleExpandIcon } from '@blocksuite/icons/rc'; +import type { DocMeta } from '@blocksuite/store'; +import * as Collapsible from '@radix-ui/react-collapsible'; +import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; +import { useMemo } from 'react'; + +import * as styles from './list.css'; +import { MasonryDocs } from './masonry'; + +const DocGroup = ({ group }: { group: ItemGroupProps }) => { + const [properties] = useAllDocDisplayProperties(); + const showTags = properties.displayProperties.tags; + + if (group.id === 'all') { + return ; + } + + return ( + + + {group.label} + + + + + + + ); +}; + +export interface AllDocListProps { + collection?: Collection; + tag?: Tag; + filters?: Filter[]; + trash?: boolean; +} + +export const AllDocList = ({ + trash, + collection, + tag, + filters = [], +}: AllDocListProps) => { + const workspace = useService(WorkspaceService).workspace; + const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection); + + const tagPageIds = useLiveData(tag?.pageIds$); + + const filteredPageMetas = useFilteredPageMetas(allPageMetas, { + trash, + filters, + collection, + }); + + const finalPageMetas = useMemo(() => { + if (tag) { + const pageIdsSet = new Set(tagPageIds); + return filteredPageMetas.filter(page => pageIdsSet.has(page.id)); + } + return filteredPageMetas; + }, [filteredPageMetas, tag, tagPageIds]); + + const groupDefs = + usePageItemGroupDefinitions() as ItemGroupDefinition[]; + + const groups = useMemo(() => { + return itemsToItemGroups(finalPageMetas ?? [], groupDefs); + }, [finalPageMetas, groupDefs]); + + return ( +
+ {groups.map(group => ( + + ))} +
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/doc/masonry.css.ts b/packages/frontend/mobile/src/views/all-docs/doc/masonry.css.ts new file mode 100644 index 000000000..52222b115 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/masonry.css.ts @@ -0,0 +1,28 @@ +import { style } from '@vanilla-extract/css'; + +export const invisibleWrapper = style({ + position: 'absolute', + padding: 'inherit', + width: '100%', + height: 0, + overflow: 'hidden', + visibility: 'hidden', + pointerEvents: 'none', +}); +export const invisibleList = style({ + width: `calc(50% - 17px / 2)`, +}); +export const stacks = style({ + position: 'relative', + width: '100%', + display: 'flex', + gap: 17, + padding: 16, +}); +export const stack = style({ + width: 0, + flex: 1, + display: 'flex', + flexDirection: 'column', + gap: 10, +}); diff --git a/packages/frontend/mobile/src/views/all-docs/doc/masonry.tsx b/packages/frontend/mobile/src/views/all-docs/doc/masonry.tsx new file mode 100644 index 000000000..8cd912911 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/masonry.tsx @@ -0,0 +1,37 @@ +import type { DocMeta } from '@blocksuite/store'; +import { useMemo } from 'react'; + +import { DocCard } from '../../../components'; +import * as styles from './masonry.css'; + +// TODO(@CatsJuice): Large amount docs performance +export const MasonryDocs = ({ + items, + showTags, +}: { + items: DocMeta[]; + showTags?: boolean; +}) => { + // card preview is loaded lazily, it's meaningless to calculate height + const stacks = useMemo(() => { + return items.reduce( + (acc, item, i) => { + acc[i % 2].push(item); + return acc; + }, + [[], []] as [DocMeta[], DocMeta[]] + ); + }, [items]); + + return ( +
+ {stacks.map((stack, i) => ( +
    + {stack.map(item => ( + + ))} +
+ ))} +
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/doc/menu.css.ts b/packages/frontend/mobile/src/views/all-docs/doc/menu.css.ts new file mode 100644 index 000000000..06f185761 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/menu.css.ts @@ -0,0 +1,67 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + flexDirection: 'column', + gap: 10, +}); +export const head = style({ + padding: '10px 20px', + fontSize: 17, + fontWeight: 600, + lineHeight: '22px', + letterSpacing: -0.43, + color: cssVarV2('text/primary'), +}); + +export const item = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: 4, + height: 34, + padding: '0 20px', + + fontSize: 17, + lineHeight: '22px', + fontWeight: 400, + letterSpacing: -0.43, +}); +export const itemSuffix = style({ + display: 'flex', + gap: 8, + alignItems: 'center', +}); +export const itemSuffixText = style({ + color: cssVarV2('text/secondary'), +}); +export const itemSuffixIcon = style({ + color: cssVarV2('icon/primary'), + fontSize: 20, +}); +export const divider = style({ + width: '100%', + height: 16, + display: 'flex', + alignItems: 'center', + ':before': { + content: "''", + height: 0.5, + width: '100%', + backgroundColor: cssVarV2('layer/insideBorder/border'), + }, +}); +export const propertiesList = style({ + padding: '4px 20px', + display: 'flex', + gap: 8, +}); +export const propertyButton = style({ + opacity: 0.4, + selectors: { + '&[data-selected="true"]': { + opacity: 1, + }, + }, +}); diff --git a/packages/frontend/mobile/src/views/all-docs/doc/menu.tsx b/packages/frontend/mobile/src/views/all-docs/doc/menu.tsx new file mode 100644 index 000000000..6fa1b5525 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/doc/menu.tsx @@ -0,0 +1,73 @@ +import { Button, MobileMenu, MobileMenuItem } from '@affine/component'; +import { + getGroupOptions, + useAllDocDisplayProperties, +} from '@affine/core/components/page-list'; +import { useI18n } from '@affine/i18n'; +import { ArrowRightSmallIcon } from '@blocksuite/icons/rc'; +import { useMemo } from 'react'; + +import * as styles from './menu.css'; + +export const AllDocsMenu = () => { + const t = useI18n(); + + const [properties, setProperties] = useAllDocDisplayProperties(); + const groupOptions = useMemo(() => getGroupOptions(t), [t]); + + const activeGroup = useMemo( + () => groupOptions.find(g => g.value === properties.groupBy), + [groupOptions, properties.groupBy] + ); + + return ( +
+
Display Settings
+ +
+ {groupOptions.map(group => ( + setProperties('groupBy', group.value)} + selected={properties.groupBy === group.value} + key={group.value} + > + {group.label} + + ))} + + } + > +
+ {t['com.affine.page.display.grouping']()} +
+ {activeGroup?.label} + +
+
+ + +
+ +
+ {t['com.affine.page.display.display-properties']()} +
+
+ +
+
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/header.tsx b/packages/frontend/mobile/src/views/all-docs/header.tsx new file mode 100644 index 000000000..f8a2127b2 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/header.tsx @@ -0,0 +1,24 @@ +import { IconButton, MobileMenu } from '@affine/component'; +import { MoreHorizontalIcon } from '@blocksuite/icons/rc'; + +import { header } from './style.css'; +import { AllDocsTabs } from './tabs'; + +export interface AllDocsHeaderProps { + operations?: React.ReactNode; +} + +export const AllDocsHeader = ({ operations }: AllDocsHeaderProps) => { + return ( +
+ +
+ {operations ? ( + + } /> + + ) : null} +
+
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/index.ts b/packages/frontend/mobile/src/views/all-docs/index.ts new file mode 100644 index 000000000..7cc2e75cf --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/index.ts @@ -0,0 +1,4 @@ +export * from './collection'; +export * from './doc/'; +export * from './header'; +export * from './tag'; diff --git a/packages/frontend/mobile/src/views/all-docs/style.css.ts b/packages/frontend/mobile/src/views/all-docs/style.css.ts new file mode 100644 index 000000000..2bd0997ba --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/style.css.ts @@ -0,0 +1,32 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const header = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 16, + padding: '16px 16px 0px 16px', + + position: 'sticky', + top: 0, + backgroundColor: cssVarV2('layer/background/secondary'), + zIndex: 1, +}); +export const tabs = style({ + height: 56, + gap: 16, + display: 'flex', + alignItems: 'center', +}); +export const tab = style({ + fontSize: 20, + fontWeight: 600, + lineHeight: '28px', + color: cssVarV2('text/tertiary'), + selectors: { + '&[data-active="true"]': { + color: cssVarV2('text/primary'), + }, + }, +}); diff --git a/packages/frontend/mobile/src/views/all-docs/tabs.tsx b/packages/frontend/mobile/src/views/all-docs/tabs.tsx new file mode 100644 index 000000000..b643e8998 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tabs.tsx @@ -0,0 +1,50 @@ +import { + WorkbenchLink, + WorkbenchService, +} from '@affine/core/modules/workbench'; +import { useLiveData, useService } from '@toeverything/infra'; + +import * as styles from './style.css'; + +interface Tab { + to: string; + label: string; +} + +const tabs: Tab[] = [ + { + to: '/all', + label: 'Docs', + }, + { + to: '/collection', + label: 'Collections', + }, + { + to: '/tag', + label: 'Tags', + }, +]; + +export const AllDocsTabs = () => { + const workbench = useService(WorkbenchService).workbench; + const location = useLiveData(workbench.location$); + + return ( +
    + {tabs.map(tab => { + return ( + + {tab.label} + + ); + })} +
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/detail-header.tsx b/packages/frontend/mobile/src/views/all-docs/tag/detail-header.tsx new file mode 100644 index 000000000..c4e13d0e2 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/detail-header.tsx @@ -0,0 +1,32 @@ +import { IconButton, MobileMenu } from '@affine/component'; +import type { Tag } from '@affine/core/modules/tag'; +import { MoreHorizontalIcon } from '@blocksuite/icons/rc'; +import { useLiveData } from '@toeverything/infra'; + +import { PageHeader } from '../../../components'; +import { AllDocsMenu } from '../doc'; +import * as styles from './detail.css'; + +export const TagDetailHeader = ({ tag }: { tag: Tag }) => { + const name = useLiveData(tag.value$); + const color = useLiveData(tag.color$); + return ( + }> + } + /> + + } + > +
+
+ {name} +
+ + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/detail.css.ts b/packages/frontend/mobile/src/views/all-docs/tag/detail.css.ts new file mode 100644 index 000000000..5d617307e --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/detail.css.ts @@ -0,0 +1,29 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const headerContent = style({ + fontSize: 17, + fontWeight: 600, + lineHeight: '22px', + letterSpacing: -0.43, + color: cssVarV2('text/primary'), + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', +}); + +export const headerIcon = style({ + width: 24, + height: 24, + marginRight: 8, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + ':before': { + content: '""', + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: 'currentColor', + }, +}); diff --git a/packages/frontend/mobile/src/views/all-docs/tag/detail.tsx b/packages/frontend/mobile/src/views/all-docs/tag/detail.tsx new file mode 100644 index 000000000..b6f0f6d1b --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/detail.tsx @@ -0,0 +1,22 @@ +import type { Tag } from '@affine/core/modules/tag'; +import { useLiveData } from '@toeverything/infra'; + +import { AppTabs } from '../../../components'; +import { AllDocList } from '../doc'; +import { TagDetailHeader } from './detail-header'; +import { TagEmpty } from './empty'; + +export const TagDetail = ({ tag }: { tag: Tag }) => { + const pageIds = useLiveData(tag?.pageIds$); + + if (!pageIds.length) { + return ; + } + return ( + <> + + + + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/empty.tsx b/packages/frontend/mobile/src/views/all-docs/tag/empty.tsx new file mode 100644 index 000000000..529b8d760 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/empty.tsx @@ -0,0 +1,12 @@ +import type { Tag } from '@affine/core/modules/tag'; + +import { TagDetailHeader } from './detail-header'; + +export const TagEmpty = ({ tag }: { tag: Tag }) => { + return ( + <> + + Empty + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/index.ts b/packages/frontend/mobile/src/views/all-docs/tag/index.ts new file mode 100644 index 000000000..3b5c1b062 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/index.ts @@ -0,0 +1,3 @@ +export { TagDetail } from './detail'; +export * from './item'; +export * from './list'; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/item.tsx b/packages/frontend/mobile/src/views/all-docs/tag/item.tsx new file mode 100644 index 000000000..fbcb20968 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/item.tsx @@ -0,0 +1,38 @@ +import { IconButton } from '@affine/component'; +import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; +import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; +import type { Tag } from '@affine/core/modules/tag'; +import { WorkbenchLink } from '@affine/core/modules/workbench'; +import { useLiveData, useService } from '@toeverything/infra'; +import { type MouseEvent, useCallback } from 'react'; + +import { content, item, prefixIcon, suffixIcon } from './styles.css'; + +export const TagItem = ({ tag }: { tag: Tag }) => { + const favAdapter = useService(CompatibleFavoriteItemsAdapter); + const isFavorite = useLiveData(favAdapter.isFavorite$(tag.id, 'tag')); + const color = useLiveData(tag.color$); + const name = useLiveData(tag.value$); + + const toggle = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + favAdapter.toggle(tag.id, 'tag'); + }, + [favAdapter, tag.id] + ); + + return ( + +
+ {name} + } + size="24" + /> + + ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/list.tsx b/packages/frontend/mobile/src/views/all-docs/tag/list.tsx new file mode 100644 index 000000000..a0761ad17 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/list.tsx @@ -0,0 +1,18 @@ +import { TagService } from '@affine/core/modules/tag'; +import { useLiveData, useService } from '@toeverything/infra'; + +import { TagItem } from './item'; +import { list } from './styles.css'; + +export const TagList = () => { + const tagList = useService(TagService).tagList; + const tags = useLiveData(tagList.tags$); + + return ( +
    + {tags.map(tag => ( + + ))} +
+ ); +}; diff --git a/packages/frontend/mobile/src/views/all-docs/tag/styles.css.ts b/packages/frontend/mobile/src/views/all-docs/tag/styles.css.ts new file mode 100644 index 000000000..148585a60 --- /dev/null +++ b/packages/frontend/mobile/src/views/all-docs/tag/styles.css.ts @@ -0,0 +1,54 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const list = style({}); +export const item = style({ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: 16, + + ':visited': { color: 'unset' }, + ':hover': { color: 'unset' }, + ':active': { color: 'unset' }, + ':focus': { color: 'unset' }, + + selectors: { + '&:not(:last-child)': { + borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`, + }, + }, +}); +export const content = style({ + width: 0, + flex: 1, + + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + + color: cssVarV2('text/primary'), + fontSize: 17, + fontWeight: 600, + lineHeight: '24px', + letterSpacing: -0.43, +}); + +export const prefixIcon = style({ + width: 24, + height: 24, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + ':before': { + content: '""', + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: 'currentColor', + }, +}); +export const suffixIcon = style({ + padding: 0, + fontSize: 24, +}); diff --git a/packages/frontend/mobile/src/views/index.ts b/packages/frontend/mobile/src/views/index.ts index 09ecfc48d..7b9275ccb 100644 --- a/packages/frontend/mobile/src/views/index.ts +++ b/packages/frontend/mobile/src/views/index.ts @@ -1,2 +1,3 @@ +export * from './all-docs'; export * from './home-header'; export * from './recent-docs'; diff --git a/packages/frontend/mobile/src/views/recent-docs/index.tsx b/packages/frontend/mobile/src/views/recent-docs/index.tsx index 6945b38b1..266c4f5b2 100644 --- a/packages/frontend/mobile/src/views/recent-docs/index.tsx +++ b/packages/frontend/mobile/src/views/recent-docs/index.tsx @@ -21,6 +21,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => { name="recent" title="Recent" headerClassName={styles.header} + className={styles.recentSection} >
    diff --git a/packages/frontend/mobile/src/views/recent-docs/styles.css.ts b/packages/frontend/mobile/src/views/recent-docs/styles.css.ts index 73cc1d08c..72e01da16 100644 --- a/packages/frontend/mobile/src/views/recent-docs/styles.css.ts +++ b/packages/frontend/mobile/src/views/recent-docs/styles.css.ts @@ -1,5 +1,13 @@ import { globalStyle, style } from '@vanilla-extract/css'; +export const recentSection = style({ + paddingBottom: 32, + selectors: { + '&[data-state="open"]': { + paddingBottom: 0, + }, + }, +}); export const scroll = style({ width: '100%', paddingTop: 8,