mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-02 16:57:07 +03:00
feat(mobile): all docs page ui impl (#7976)
This commit is contained in:
parent
db76780bc9
commit
3ce92f2abc
@ -18,6 +18,31 @@ type GroupOption = {
|
||||
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 = () => {
|
||||
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 => (
|
||||
<MenuItem
|
||||
|
@ -19,6 +19,10 @@ export const getPagePreviewText = (page: Doc) => {
|
||||
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<string>('');
|
||||
|
@ -14,11 +14,12 @@ export type WorkbenchLinkProps = React.PropsWithChildren<
|
||||
{
|
||||
to: To;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
replaceHistory?: boolean;
|
||||
} & React.HTMLProps<HTMLAnchorElement>
|
||||
>;
|
||||
|
||||
export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
|
||||
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<HTMLAnchorElement, WorkbenchLinkProps>(
|
||||
}
|
||||
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
|
||||
|
@ -121,7 +121,7 @@ export const Component = function CollectionPage() {
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
const inner = isEmpty(collection) ? (
|
||||
const inner = isEmptyCollection(collection) ? (
|
||||
<Placeholder collection={collection} />
|
||||
) : (
|
||||
<CollectionDetail collection={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
|
||||
);
|
||||
|
@ -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 = () => {
|
||||
<ul className={styles.appTabs} id="app-tabs">
|
||||
{routes.map(route => {
|
||||
const Link = route.LinkComponent || WorkbenchLink;
|
||||
|
||||
const isActive = route.isActive
|
||||
? route.isActive(location)
|
||||
: location.pathname === route.to;
|
||||
return (
|
||||
<Link
|
||||
data-active={location.pathname === route.to}
|
||||
data-active={isActive}
|
||||
to={route.to}
|
||||
key={route.to}
|
||||
className={styles.tabItem}
|
||||
|
@ -18,6 +18,7 @@ export const appTabs = style({
|
||||
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
});
|
||||
export const tabItem = style({
|
||||
display: 'flex',
|
||||
|
@ -16,10 +16,11 @@ import { DocCardTags } from './tag';
|
||||
|
||||
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
||||
meta: DocMeta;
|
||||
showTags?: boolean;
|
||||
}
|
||||
|
||||
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 workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
@ -54,7 +55,7 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
||||
emptyFallback={<div className={styles.contentEmpty}>Empty</div>}
|
||||
/>
|
||||
</main>
|
||||
<DocCardTags docId={meta.id} rows={2} />
|
||||
{showTags ? <DocCardTags docId={meta.id} rows={2} /> : null}
|
||||
</WorkbenchLink>
|
||||
);
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ const DocCardTagsRenderer = ({ tags, rows }: { tags: Tag[]; rows: number }) => {
|
||||
{tags.map(tag => (
|
||||
<DocCardTag key={tag.id} tag={tag} />
|
||||
))}
|
||||
{/* TODO: more icon */}
|
||||
{/* TODO(@CatsJuice): more icon */}
|
||||
{/* <MoreHorizontalIcon /> */}
|
||||
</ul>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './app-tabs';
|
||||
export * from './doc-card';
|
||||
export * from './page-header';
|
||||
export * from './search-button';
|
||||
export * from './workspace-selector';
|
||||
|
@ -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,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { AppTabs } from '../../components';
|
||||
import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views';
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<>
|
||||
`workspace/all.tsx`;
|
||||
<AllDocsHeader operations={<AllDocsMenu />} />
|
||||
<AllDocList />
|
||||
<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 = () => {
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
return <div>/workspace/:workspaceId/tag</div>;
|
||||
return (
|
||||
<>
|
||||
<AllDocsHeader />
|
||||
<AppTabs />
|
||||
<TagList />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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]);
|
3
packages/frontend/mobile/src/views/all-docs/doc/index.ts
Normal file
3
packages/frontend/mobile/src/views/all-docs/doc/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './list';
|
||||
export * from './masonry';
|
||||
export * from './menu';
|
32
packages/frontend/mobile/src/views/all-docs/doc/list.css.ts
Normal file
32
packages/frontend/mobile/src/views/all-docs/doc/list.css.ts
Normal file
@ -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,
|
||||
});
|
88
packages/frontend/mobile/src/views/all-docs/doc/list.tsx
Normal file
88
packages/frontend/mobile/src/views/all-docs/doc/list.tsx
Normal file
@ -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,
|
||||
});
|
37
packages/frontend/mobile/src/views/all-docs/doc/masonry.tsx
Normal file
37
packages/frontend/mobile/src/views/all-docs/doc/masonry.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
67
packages/frontend/mobile/src/views/all-docs/doc/menu.css.ts
Normal file
67
packages/frontend/mobile/src/views/all-docs/doc/menu.css.ts
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
73
packages/frontend/mobile/src/views/all-docs/doc/menu.tsx
Normal file
73
packages/frontend/mobile/src/views/all-docs/doc/menu.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
24
packages/frontend/mobile/src/views/all-docs/header.tsx
Normal file
24
packages/frontend/mobile/src/views/all-docs/header.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
4
packages/frontend/mobile/src/views/all-docs/index.ts
Normal file
4
packages/frontend/mobile/src/views/all-docs/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './collection';
|
||||
export * from './doc/';
|
||||
export * from './header';
|
||||
export * from './tag';
|
32
packages/frontend/mobile/src/views/all-docs/style.css.ts
Normal file
32
packages/frontend/mobile/src/views/all-docs/style.css.ts
Normal file
@ -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'),
|
||||
},
|
||||
},
|
||||
});
|
50
packages/frontend/mobile/src/views/all-docs/tabs.tsx
Normal file
50
packages/frontend/mobile/src/views/all-docs/tabs.tsx
Normal file
@ -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',
|
||||
},
|
||||
});
|
22
packages/frontend/mobile/src/views/all-docs/tag/detail.tsx
Normal file
22
packages/frontend/mobile/src/views/all-docs/tag/detail.tsx
Normal file
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
12
packages/frontend/mobile/src/views/all-docs/tag/empty.tsx
Normal file
12
packages/frontend/mobile/src/views/all-docs/tag/empty.tsx
Normal file
@ -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
|
||||
</>
|
||||
);
|
||||
};
|
3
packages/frontend/mobile/src/views/all-docs/tag/index.ts
Normal file
3
packages/frontend/mobile/src/views/all-docs/tag/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { TagDetail } from './detail';
|
||||
export * from './item';
|
||||
export * from './list';
|
38
packages/frontend/mobile/src/views/all-docs/tag/item.tsx
Normal file
38
packages/frontend/mobile/src/views/all-docs/tag/item.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
18
packages/frontend/mobile/src/views/all-docs/tag/list.tsx
Normal file
18
packages/frontend/mobile/src/views/all-docs/tag/list.tsx
Normal file
@ -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 './recent-docs';
|
||||
|
@ -21,6 +21,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
|
||||
name="recent"
|
||||
title="Recent"
|
||||
headerClassName={styles.header}
|
||||
className={styles.recentSection}
|
||||
>
|
||||
<div className={styles.scroll}>
|
||||
<ul className={styles.list}>
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user