mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 04:51:49 +08:00
feat(mobile): all docs page ui impl (#7976)
This commit is contained in:
@@ -18,6 +18,31 @@ type GroupOption = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getGroupOptions(t: ReturnType<typeof useI18n>) {
|
||||||
|
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 = () => {
|
export const PageDisplayMenu = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [workspaceProperties, setProperties] = useAllDocDisplayProperties();
|
const [workspaceProperties, setProperties] = useAllDocDisplayProperties();
|
||||||
@@ -66,28 +91,7 @@ export const PageDisplayMenu = () => {
|
|||||||
}, [handleSetDocDisplayProperties, t]);
|
}, [handleSetDocDisplayProperties, t]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const groupOptions: GroupOption[] = [
|
const groupOptions: GroupOption[] = getGroupOptions(t);
|
||||||
{
|
|
||||||
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 subItems = groupOptions.map(option => (
|
const subItems = groupOptions.map(option => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export const getPagePreviewText = (page: Doc) => {
|
|||||||
let count = MAX_SEARCH_BLOCK_COUNT;
|
let count = MAX_SEARCH_BLOCK_COUNT;
|
||||||
while (queue.length && previewLenNeeded > 0 && count-- > 0) {
|
while (queue.length && previewLenNeeded > 0 && count-- > 0) {
|
||||||
const block = queue.shift();
|
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) {
|
if (!block) {
|
||||||
console.error('Unexpected empty block');
|
console.error('Unexpected empty block');
|
||||||
break;
|
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<string>('');
|
const emptyAtom = atom<string>('');
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ export type WorkbenchLinkProps = React.PropsWithChildren<
|
|||||||
{
|
{
|
||||||
to: To;
|
to: To;
|
||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
replaceHistory?: boolean;
|
||||||
} & React.HTMLProps<HTMLAnchorElement>
|
} & React.HTMLProps<HTMLAnchorElement>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
|
export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
|
||||||
function WorkbenchLink({ to, onClick, ...other }, ref) {
|
function WorkbenchLink({ to, onClick, replaceHistory, ...other }, ref) {
|
||||||
const { featureFlagService, workbenchService } = useServices({
|
const { featureFlagService, workbenchService } = useServices({
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
@@ -45,10 +46,10 @@ export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
|
|||||||
}
|
}
|
||||||
return 'active';
|
return 'active';
|
||||||
})();
|
})();
|
||||||
workbench.open(to, { at });
|
workbench.open(to, { at, replaceHistory });
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
[enableMultiView, onClick, to, workbench]
|
[enableMultiView, onClick, replaceHistory, to, workbench]
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint suspicious runtime error
|
// eslint suspicious runtime error
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const Component = function CollectionPage() {
|
|||||||
if (!collection) {
|
if (!collection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const inner = isEmpty(collection) ? (
|
const inner = isEmptyCollection(collection) ? (
|
||||||
<Placeholder collection={collection} />
|
<Placeholder collection={collection} />
|
||||||
) : (
|
) : (
|
||||||
<CollectionDetail collection={collection} />
|
<CollectionDetail collection={collection} />
|
||||||
@@ -346,7 +346,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmpty = (collection: Collection) => {
|
export const isEmptyCollection = (collection: Collection) => {
|
||||||
return (
|
return (
|
||||||
collection.allowList.length === 0 && collection.filterList.length === 0
|
collection.allowList.length === 0 && collection.filterList.length === 0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { AllDocsIcon, SearchIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import type { Location } from 'react-router-dom';
|
||||||
|
|
||||||
import { HomeIcon } from './home-icon';
|
import { HomeIcon } from './home-icon';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -12,6 +13,7 @@ interface Route {
|
|||||||
to: string;
|
to: string;
|
||||||
Icon: React.FC;
|
Icon: React.FC;
|
||||||
LinkComponent?: React.FC;
|
LinkComponent?: React.FC;
|
||||||
|
isActive?: (location: Location) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = [
|
||||||
@@ -22,6 +24,10 @@ const routes: Route[] = [
|
|||||||
{
|
{
|
||||||
to: '/all',
|
to: '/all',
|
||||||
Icon: AllDocsIcon,
|
Icon: AllDocsIcon,
|
||||||
|
isActive: location =>
|
||||||
|
location.pathname === '/all' ||
|
||||||
|
location.pathname.startsWith('/collection') ||
|
||||||
|
location.pathname.startsWith('/tag'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/search',
|
to: '/search',
|
||||||
@@ -37,9 +43,13 @@ export const AppTabs = () => {
|
|||||||
<ul className={styles.appTabs} id="app-tabs">
|
<ul className={styles.appTabs} id="app-tabs">
|
||||||
{routes.map(route => {
|
{routes.map(route => {
|
||||||
const Link = route.LinkComponent || WorkbenchLink;
|
const Link = route.LinkComponent || WorkbenchLink;
|
||||||
|
|
||||||
|
const isActive = route.isActive
|
||||||
|
? route.isActive(location)
|
||||||
|
: location.pathname === route.to;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
data-active={location.pathname === route.to}
|
data-active={isActive}
|
||||||
to={route.to}
|
to={route.to}
|
||||||
key={route.to}
|
key={route.to}
|
||||||
className={styles.tabItem}
|
className={styles.tabItem}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const appTabs = style({
|
|||||||
|
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
export const tabItem = style({
|
export const tabItem = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ import { DocCardTags } from './tag';
|
|||||||
|
|
||||||
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
||||||
meta: DocMeta;
|
meta: DocMeta;
|
||||||
|
showTags?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
||||||
function DocCard({ meta, className, ...attrs }, ref) {
|
function DocCard({ showTags = true, meta, className, ...attrs }, ref) {
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
|||||||
emptyFallback={<div className={styles.contentEmpty}>Empty</div>}
|
emptyFallback={<div className={styles.contentEmpty}>Empty</div>}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<DocCardTags docId={meta.id} rows={2} />
|
{showTags ? <DocCardTags docId={meta.id} rows={2} /> : null}
|
||||||
</WorkbenchLink>
|
</WorkbenchLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const DocCardTagsRenderer = ({ tags, rows }: { tags: Tag[]; rows: number }) => {
|
|||||||
{tags.map(tag => (
|
{tags.map(tag => (
|
||||||
<DocCardTag key={tag.id} tag={tag} />
|
<DocCardTag key={tag.id} tag={tag} />
|
||||||
))}
|
))}
|
||||||
{/* TODO: more icon */}
|
{/* TODO(@CatsJuice): more icon */}
|
||||||
{/* <MoreHorizontalIcon /> */}
|
{/* <MoreHorizontalIcon /> */}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './app-tabs';
|
export * from './app-tabs';
|
||||||
|
export * from './doc-card';
|
||||||
export * from './page-header';
|
export * from './page-header';
|
||||||
export * from './search-button';
|
export * from './search-button';
|
||||||
export * from './workspace-selector';
|
export * from './workspace-selector';
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: 44,
|
minHeight: 44,
|
||||||
padding: '0 6px',
|
padding: '0 6px',
|
||||||
|
paddingTop: 16,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
position: 'relative',
|
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||||
});
|
});
|
||||||
export const content = style({
|
export const content = style({
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -15,6 +21,11 @@ export const content = style({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: 'calc(100% - 12px - 88px - 16px)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
'&:not(.center)': {
|
'&:not(.center)': {
|
||||||
width: 0,
|
width: 0,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { AppTabs } from '../../components';
|
import { AppTabs } from '../../components';
|
||||||
|
import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
`workspace/all.tsx`;
|
<AllDocsHeader operations={<AllDocsMenu />} />
|
||||||
|
<AllDocList />
|
||||||
<AppTabs />
|
<AppTabs />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = () => {
|
export const Component = () => {
|
||||||
return <div>/workspace/:workspaceId/collection/:collectionId</div>;
|
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 <EmptyCollection collection={collection} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CollectionDetail collection={collection} />
|
||||||
|
<AppTabs />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import { AppTabs } from '../../../components';
|
||||||
|
import { AllDocsHeader, CollectionList } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
return <div>/workspace/:workspaceId/collection</div>;
|
return (
|
||||||
|
<>
|
||||||
|
<AllDocsHeader />
|
||||||
|
<AppTabs />
|
||||||
|
<CollectionList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = () => {
|
export const Component = () => {
|
||||||
return <div>/workspace/:workspaceId/tag/:tagId</div>;
|
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 <PageNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TagDetail tag={currentTag} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import { AppTabs } from '../../../components';
|
||||||
|
import { AllDocsHeader, TagList } from '../../../views';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
return <div>/workspace/:workspaceId/tag</div>;
|
return (
|
||||||
|
<>
|
||||||
|
<AllDocsHeader />
|
||||||
|
<AppTabs />
|
||||||
|
<TagList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { Component as All } from './pages/workspace/all';
|
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 Home } from './pages/workspace/home';
|
||||||
import { Component as Search } from './pages/workspace/search';
|
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 = [
|
export const topLevelRoutes = [
|
||||||
{
|
{
|
||||||
@@ -67,19 +71,23 @@ export const viewRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/collection',
|
path: '/collection',
|
||||||
lazy: () => import('./pages/workspace/collection/index'),
|
// lazy: () => import('./pages/workspace/collection/index'),
|
||||||
|
Component: Collection,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/collection/:collectionId',
|
path: '/collection/:collectionId',
|
||||||
lazy: () => import('./pages/workspace/collection/detail'),
|
// lazy: () => import('./pages/workspace/collection/detail'),
|
||||||
|
Component: CollectionDetail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tag',
|
path: '/tag',
|
||||||
lazy: () => import('./pages/workspace/tag/index'),
|
// lazy: () => import('./pages/workspace/tag/index'),
|
||||||
|
Component: Tag,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tag/:tagId',
|
path: '/tag/:tagId',
|
||||||
lazy: () => import('./pages/workspace/tag/detail'),
|
// lazy: () => import('./pages/workspace/tag/detail'),
|
||||||
|
Component: TagDetail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/trash',
|
path: '/trash',
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<PageHeader
|
||||||
|
back
|
||||||
|
suffix={
|
||||||
|
<MobileMenu items={<AllDocsMenu />}>
|
||||||
|
<IconButton
|
||||||
|
size="24"
|
||||||
|
style={{ padding: 10 }}
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
/>
|
||||||
|
</MobileMenu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.headerContent}>
|
||||||
|
<ViewLayersIcon className={styles.headerIcon} />
|
||||||
|
{collection.name}
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CollectionDetail = ({
|
||||||
|
collection,
|
||||||
|
}: {
|
||||||
|
collection: Collection;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailHeader collection={collection} />
|
||||||
|
<AllDocList collection={collection} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Collection } from '@affine/env/filter';
|
||||||
|
|
||||||
|
import { DetailHeader } from './detail';
|
||||||
|
|
||||||
|
export const EmptyCollection = ({ collection }: { collection: Collection }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailHeader collection={collection} />
|
||||||
|
Empty
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { CollectionDetail } from './detail';
|
||||||
|
export * from './empty';
|
||||||
|
export * from './list';
|
||||||
@@ -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 (
|
||||||
|
<WorkbenchLink to={`/collection/${meta.id}`} className={item}>
|
||||||
|
<ViewLayersIcon className={prefixIcon} />
|
||||||
|
<span className={name}>{meta.title}</span>
|
||||||
|
<IconButton
|
||||||
|
className={suffixIcon}
|
||||||
|
onClick={toggle}
|
||||||
|
icon={<IsFavoriteIcon favorite={isFavorite} />}
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
|
</WorkbenchLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<ul className={list}>
|
||||||
|
{collectionMetas.map(meta => (
|
||||||
|
<CollectionListItem key={meta.id} meta={meta} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './list';
|
||||||
|
export * from './masonry';
|
||||||
|
export * from './menu';
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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<DocMeta> }) => {
|
||||||
|
const [properties] = useAllDocDisplayProperties();
|
||||||
|
const showTags = properties.displayProperties.tags;
|
||||||
|
|
||||||
|
if (group.id === 'all') {
|
||||||
|
return <MasonryDocs items={group.items} showTags={showTags} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible.Root defaultOpen>
|
||||||
|
<Collapsible.Trigger className={styles.groupTitle}>
|
||||||
|
{group.label}
|
||||||
|
<ToggleExpandIcon className={styles.groupTitleIcon} />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<MasonryDocs items={group.items} showTags={showTags} />
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<DocMeta>[];
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
return itemsToItemGroups(finalPageMetas ?? [], groupDefs);
|
||||||
|
}, [finalPageMetas, groupDefs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{groups.map(group => (
|
||||||
|
<DocGroup key={group.id} group={group} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.stacks}>
|
||||||
|
{stacks.map((stack, i) => (
|
||||||
|
<ul key={i} className={styles.stack}>
|
||||||
|
{stack.map(item => (
|
||||||
|
<DocCard showTags={showTags} key={item.id} meta={item} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<header className={styles.head}>Display Settings</header>
|
||||||
|
<MobileMenu
|
||||||
|
items={
|
||||||
|
<>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
{groupOptions.map(group => (
|
||||||
|
<MobileMenuItem
|
||||||
|
onSelect={() => setProperties('groupBy', group.value)}
|
||||||
|
selected={properties.groupBy === group.value}
|
||||||
|
key={group.value}
|
||||||
|
>
|
||||||
|
{group.label}
|
||||||
|
</MobileMenuItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<span>{t['com.affine.page.display.grouping']()}</span>
|
||||||
|
<div className={styles.itemSuffix}>
|
||||||
|
<span className={styles.itemSuffixText}>{activeGroup?.label}</span>
|
||||||
|
<ArrowRightSmallIcon className={styles.itemSuffixIcon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MobileMenu>
|
||||||
|
|
||||||
|
<div className={styles.divider} />
|
||||||
|
|
||||||
|
<div className={styles.item}>
|
||||||
|
{t['com.affine.page.display.display-properties']()}
|
||||||
|
</div>
|
||||||
|
<div className={styles.propertiesList}>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
className={styles.propertyButton}
|
||||||
|
data-selected={properties.displayProperties.tags}
|
||||||
|
onClick={() =>
|
||||||
|
setProperties('displayProperties', {
|
||||||
|
...properties.displayProperties,
|
||||||
|
tags: !properties.displayProperties.tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<header className={header}>
|
||||||
|
<AllDocsTabs />
|
||||||
|
<div>
|
||||||
|
{operations ? (
|
||||||
|
<MobileMenu items={operations}>
|
||||||
|
<IconButton icon={<MoreHorizontalIcon />} />
|
||||||
|
</MobileMenu>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './collection';
|
||||||
|
export * from './doc/';
|
||||||
|
export * from './header';
|
||||||
|
export * from './tag';
|
||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<ul className={styles.tabs}>
|
||||||
|
{tabs.map(tab => {
|
||||||
|
return (
|
||||||
|
<WorkbenchLink
|
||||||
|
data-active={location.pathname === tab.to}
|
||||||
|
replaceHistory
|
||||||
|
className={styles.tab}
|
||||||
|
key={tab.to}
|
||||||
|
to={tab.to}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</WorkbenchLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<PageHeader
|
||||||
|
back
|
||||||
|
suffix={
|
||||||
|
<MobileMenu items={<AllDocsMenu />}>
|
||||||
|
<IconButton
|
||||||
|
size="24"
|
||||||
|
style={{ padding: 10 }}
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
/>
|
||||||
|
</MobileMenu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.headerContent}>
|
||||||
|
<div className={styles.headerIcon} style={{ color }} />
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 <TagEmpty tag={tag} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TagDetailHeader tag={tag} />
|
||||||
|
<AllDocList tag={tag} />
|
||||||
|
<AppTabs />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
|
|
||||||
|
import { TagDetailHeader } from './detail-header';
|
||||||
|
|
||||||
|
export const TagEmpty = ({ tag }: { tag: Tag }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TagDetailHeader tag={tag} />
|
||||||
|
Empty
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { TagDetail } from './detail';
|
||||||
|
export * from './item';
|
||||||
|
export * from './list';
|
||||||
@@ -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 (
|
||||||
|
<WorkbenchLink to={`/tag/${tag.id}`} className={item}>
|
||||||
|
<div className={prefixIcon} style={{ color }} />
|
||||||
|
<span className={content}>{name}</span>
|
||||||
|
<IconButton
|
||||||
|
className={suffixIcon}
|
||||||
|
onClick={toggle}
|
||||||
|
icon={<IsFavoriteIcon favorite={isFavorite} />}
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
|
</WorkbenchLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<ul className={list}>
|
||||||
|
{tags.map(tag => (
|
||||||
|
<TagItem key={tag.id} tag={tag} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './all-docs';
|
||||||
export * from './home-header';
|
export * from './home-header';
|
||||||
export * from './recent-docs';
|
export * from './recent-docs';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
|
|||||||
name="recent"
|
name="recent"
|
||||||
title="Recent"
|
title="Recent"
|
||||||
headerClassName={styles.header}
|
headerClassName={styles.header}
|
||||||
|
className={styles.recentSection}
|
||||||
>
|
>
|
||||||
<div className={styles.scroll}>
|
<div className={styles.scroll}>
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const recentSection = style({
|
||||||
|
paddingBottom: 32,
|
||||||
|
selectors: {
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
export const scroll = style({
|
export const scroll = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
|
|||||||
Reference in New Issue
Block a user