diff --git a/packages/frontend/component/src/index.ts b/packages/frontend/component/src/index.ts index 083eb6b216..a0abca5a08 100644 --- a/packages/frontend/component/src/index.ts +++ b/packages/frontend/component/src/index.ts @@ -17,6 +17,7 @@ export * from './ui/loading'; export * from './ui/lottie/collections-icon'; export * from './ui/lottie/delete-icon'; export * from './ui/lottie/folder-icon'; +export * from './ui/masonry'; export * from './ui/menu'; export * from './ui/modal'; export * from './ui/notification'; diff --git a/packages/frontend/component/src/ui/masonry/context.ts b/packages/frontend/component/src/ui/masonry/context.ts new file mode 100644 index 0000000000..1c9bfe0389 --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/context.ts @@ -0,0 +1,29 @@ +import { createContext } from 'react'; + +export interface MasonryItem { + id: string; + height: number; +} + +export interface LayoutInfo { + x: number; + y: number; + w: number; + h: number; +} + +export interface MasonryContext { + addItem: (item: MasonryItem, index?: number) => void; + removeItem: (id: string) => void; + items: MasonryItem[]; + layoutMap: Map; + sleepMap: Map; +} + +export const MasonryContext = createContext({ + addItem: () => {}, + removeItem: () => {}, + items: [], + layoutMap: new Map(), + sleepMap: new Map(), +}); diff --git a/packages/frontend/component/src/ui/masonry/index.ts b/packages/frontend/component/src/ui/masonry/index.ts new file mode 100644 index 0000000000..a6eef75fb5 --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/index.ts @@ -0,0 +1,7 @@ +import { MasonryItem } from './masonry-item'; +import { MasonryRoot } from './masonry-root'; + +export const Masonry = { + Item: MasonryItem, + Root: MasonryRoot, +}; diff --git a/packages/frontend/component/src/ui/masonry/masonry-item.tsx b/packages/frontend/component/src/ui/masonry/masonry-item.tsx new file mode 100644 index 0000000000..636b18a298 --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/masonry-item.tsx @@ -0,0 +1,55 @@ +import clsx from 'clsx'; +import { useContext, useEffect, useMemo } from 'react'; + +import { MasonryContext, type MasonryItem as MasonryItemType } from './context'; +import * as styles from './styles.css'; +export interface MasonryItemProps + extends Omit, 'id' | 'height'>, + MasonryItemType {} + +export const MasonryItem = ({ + id, + height, + className, + children, + style: styleProp, + ...props +}: MasonryItemProps) => { + const { addItem, removeItem, layoutMap, sleepMap } = + useContext(MasonryContext); + + useEffect(() => { + addItem({ id, height }); + return () => { + removeItem(id); + }; + }, [addItem, height, id, removeItem]); + + const style = useMemo(() => { + const info = layoutMap.get(id); + if (!info) return { display: 'none' }; + + const { x, y, w, h } = info; + return { + ...styleProp, + left: `${x}px`, + top: `${y}px`, + width: `${w}px`, + height: `${h}px`, + }; + }, [id, layoutMap, styleProp]); + + if (sleepMap.get(id)) return null; + if (!layoutMap.get(id)) return null; + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/frontend/component/src/ui/masonry/masonry-root.tsx b/packages/frontend/component/src/ui/masonry/masonry-root.tsx new file mode 100644 index 0000000000..f41b38a555 --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/masonry-root.tsx @@ -0,0 +1,187 @@ +import clsx from 'clsx'; +import { debounce } from 'lodash-es'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { observeResize } from '../../utils'; +import { Scrollable } from '../scrollbar'; +import { type LayoutInfo, MasonryContext, type MasonryItem } from './context'; +import * as styles from './styles.css'; +import { calcColumns, calcLayout, calcSleep } from './utils'; + +export interface MasonryRootProps extends React.HTMLAttributes { + gapX: number; + gapY: number; + paddingX?: number; + paddingY?: number; + /** + * Specify the width of the item. + * - `number`: The width of the item in pixels. + * - `'stretch'`: The item will stretch to fill the container. + * @default 'stretch' + */ + itemWidth?: number | 'stretch'; + /** + * The minimum width of the item in pixels. + * @default 100 + */ + itemWidthMin?: number; + virtualScroll?: boolean; +} + +export const MasonryRoot = ({ + gapX, + gapY, + itemWidth = 'stretch', + itemWidthMin = 100, + paddingX = 0, + paddingY = 0, + children, + style, + className, + virtualScroll = false, + ...props +}: MasonryRootProps) => { + const rootRef = useRef(null); + const [items, setItems] = useState([]); + const [height, setHeight] = useState(0); + const [layoutMap, setLayoutMap] = useState< + Map + >(new Map()); + const [sleepMap, setSleepMap] = useState>( + new Map() + ); + + const addItem = useCallback((item: MasonryItem, index?: number) => { + if (index === undefined) { + setItems(prev => [...prev, item]); + } else { + setItems(prev => { + const limitedIndex = Math.max(0, Math.min(index, prev.length)); + return [ + ...prev.slice(0, limitedIndex), + item, + ...prev.slice(limitedIndex), + ]; + }); + } + }, []); + + const removeItem = useCallback((id: string) => { + setItems(prev => prev.filter(item => item.id !== id)); + }, []); + + const contextValue = useMemo( + () => ({ + addItem, + removeItem, + items, + layoutMap, + sleepMap, + }), + [items, addItem, removeItem, layoutMap, sleepMap] + ); + + const updateSleepMap = useCallback( + (layoutMap: Map, _scrollY?: number) => { + if (!virtualScroll) return; + + const rootEl = rootRef.current; + if (!rootEl) return; + + const scrollY = _scrollY ?? rootEl.scrollTop; + + const sleepMap = calcSleep({ + viewportHeight: rootEl.clientHeight, + scrollY, + layoutMap, + preloadHeight: 50, + }); + setSleepMap(sleepMap); + }, + [virtualScroll] + ); + + const calculateLayout = useCallback(() => { + const rootEl = rootRef.current; + if (!rootEl) return; + + const totalWidth = rootEl.clientWidth; + const { columns, width } = calcColumns( + totalWidth, + itemWidth, + itemWidthMin, + gapX, + paddingX + ); + + const { layout, height } = calcLayout(items, { + columns, + width, + gapX, + gapY, + paddingX, + paddingY, + }); + setLayoutMap(layout); + setHeight(height); + updateSleepMap(layout); + }, [ + gapX, + gapY, + itemWidth, + itemWidthMin, + items, + paddingX, + paddingY, + updateSleepMap, + ]); + + useEffect(() => { + calculateLayout(); + if (rootRef.current) { + return observeResize(rootRef.current, calculateLayout); + } + return; + }, [calculateLayout]); + + useEffect(() => { + const rootEl = rootRef.current; + if (!rootEl) return; + + if (virtualScroll) { + const handler = debounce((e: Event) => { + const scrollY = (e.target as HTMLElement).scrollTop; + updateSleepMap(layoutMap, scrollY); + }, 50); + rootEl.addEventListener('scroll', handler); + return () => { + rootEl.removeEventListener('scroll', handler); + }; + } + return; + }, [layoutMap, updateSleepMap, virtualScroll]); + + return ( + + + + {children} +
+ + + + + ); +}; diff --git a/packages/frontend/component/src/ui/masonry/masonry.stories.tsx b/packages/frontend/component/src/ui/masonry/masonry.stories.tsx new file mode 100644 index 0000000000..cf655077ff --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/masonry.stories.tsx @@ -0,0 +1,55 @@ +import { ResizePanel } from '../resize-panel/resize-panel'; +import { MasonryItem } from './masonry-item'; +import { MasonryRoot } from './masonry-root'; + +export default { + title: 'UI/Masonry', +}; + +const Card = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +const cards = Array.from({ length: 10000 }, (_, i) => { + return { + id: 'card-' + i, + height: Math.round(100 + Math.random() * 100), + }; +}); + +export const BasicVirtualScroll = () => { + return ( + + + {cards.map(card => ( + + +

Hello

+

World

+ {card.id} +
+
+ ))} +
+
+ ); +}; diff --git a/packages/frontend/component/src/ui/masonry/styles.css.ts b/packages/frontend/component/src/ui/masonry/styles.css.ts new file mode 100644 index 0000000000..7435237bfa --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/styles.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + position: 'relative', + selectors: { + '&.scrollable': { + overflowY: 'auto', + }, + }, +}); + +export const item = style({ + position: 'absolute', +}); diff --git a/packages/frontend/component/src/ui/masonry/utils.ts b/packages/frontend/component/src/ui/masonry/utils.ts new file mode 100644 index 0000000000..9dca3f35b0 --- /dev/null +++ b/packages/frontend/component/src/ui/masonry/utils.ts @@ -0,0 +1,87 @@ +import type { LayoutInfo, MasonryItem } from './context'; + +export const calcColumns = ( + totalWidth: number, + itemWidth: number | 'stretch', + itemWidthMin: number, + gapX: number, + paddingX: number +) => { + const availableWidth = totalWidth - paddingX * 2; + + if (itemWidth === 'stretch') { + let columns = 1; + while (columns * itemWidthMin + (columns - 1) * gapX < availableWidth) { + columns++; + } + const finalColumns = columns - 1; + const finalWidth = + (availableWidth - (finalColumns - 1) * gapX) / finalColumns; + return { + columns: finalColumns, + width: finalWidth, + }; + } else { + let columns = 1; + while (columns * itemWidth + (columns - 1) * gapX < availableWidth) { + columns++; + } + return { + columns: columns - 1, + width: itemWidth, + }; + } +}; + +export const calcLayout = ( + items: MasonryItem[], + options: { + columns: number; + width: number; + gapX: number; + gapY: number; + paddingX: number; + paddingY: number; + } +) => { + const { columns, width, gapX, gapY, paddingX, paddingY } = options; + + const layoutMap = new Map(); + const heightStack = Array.from({ length: columns }, () => paddingY); + + items.forEach(item => { + const minHeight = Math.min(...heightStack); + const minHeightIndex = heightStack.indexOf(minHeight); + const x = minHeightIndex * (width + gapX) + paddingX; + const y = minHeight + gapY; + heightStack[minHeightIndex] = y + item.height; + layoutMap.set(item.id, { x, y, w: width, h: item.height }); + }); + + const finalHeight = Math.max(...heightStack) + paddingY; + + return { layout: layoutMap, height: finalHeight }; +}; + +export const calcSleep = (options: { + viewportHeight: number; + scrollY: number; + layoutMap: Map; + preloadHeight: number; +}) => { + const { viewportHeight, scrollY, layoutMap, preloadHeight } = options; + + const sleepMap = new Map(); + + layoutMap.forEach((layout, id) => { + const { y, h } = layout; + + const isInView = + y + h + preloadHeight > scrollY && + y - preloadHeight < scrollY + viewportHeight; + + sleepMap.set(id, !isInView); + }); + + return sleepMap; +}; diff --git a/packages/frontend/core/src/mobile/components/app-tabs/index.tsx b/packages/frontend/core/src/mobile/components/app-tabs/index.tsx index 1a5de84202..b11f935b61 100644 --- a/packages/frontend/core/src/mobile/components/app-tabs/index.tsx +++ b/packages/frontend/core/src/mobile/components/app-tabs/index.tsx @@ -10,15 +10,23 @@ import { type AppTabLink, tabs } from './data'; import * as styles from './styles.css'; import { TabItem } from './tab-item'; -export const AppTabs = ({ background }: { background?: string }) => { +export const AppTabs = ({ + background, + fixed = true, +}: { + background?: string; + fixed?: boolean; +}) => { const virtualKeyboardService = useService(VirtualKeyboardService); const virtualKeyboardVisible = useLiveData(virtualKeyboardService.show$); - return createPortal( + const tab = ( { visibility: virtualKeyboardVisible ? 'hidden' : 'visible', }} > -
    +
      {tabs.map(tab => { if ('to' in tab) { return ; @@ -39,9 +47,10 @@ export const AppTabs = ({ background }: { background?: string }) => { } })}
    - , - document.body + ); + + return fixed ? createPortal(tab, document.body) : tab; }; const AppTabLink = ({ route }: { route: AppTabLink }) => { diff --git a/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts b/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts index 96f29015d3..26f676fd7f 100644 --- a/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts +++ b/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts @@ -14,9 +14,16 @@ export const appTabs = style({ width: '100dvw', - position: 'fixed', - bottom: -2, zIndex: 1, + + marginBottom: -2, + selectors: { + '&[data-fixed="true"]': { + position: 'fixed', + bottom: -2, + marginBottom: 0, + }, + }, }); export const appTabsInner = style({ display: 'flex', diff --git a/packages/frontend/core/src/mobile/components/doc-card/index.tsx b/packages/frontend/core/src/mobile/components/doc-card/index.tsx index f7cf3abd9a..8678be2cc8 100644 --- a/packages/frontend/core/src/mobile/components/doc-card/index.tsx +++ b/packages/frontend/core/src/mobile/components/doc-card/index.tsx @@ -1,4 +1,4 @@ -import { IconButton, observeIntersection, Skeleton } from '@affine/component'; +import { IconButton, Skeleton } from '@affine/component'; import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook'; import { PagePreview } from '@affine/core/components/page-list/page-content-preview'; import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; @@ -11,14 +11,7 @@ import { import type { DocMeta } from '@blocksuite/affine/store'; import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; -import { - forwardRef, - type ReactNode, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { forwardRef, type ReactNode, useMemo, useRef } from 'react'; import * as styles from './styles.css'; import { DocCardTags } from './tag'; @@ -66,20 +59,6 @@ export const DocCard = forwardRef( return { height: `${rows * 18}px` }; }, [autoHeightById, meta.id]); - const [visible, setVisible] = useState(false); - - useEffect(() => { - if (!containerRef.current) return; - - const dispose = observeIntersection(containerRef.current, entry => { - setVisible(entry.isIntersecting); - }); - - return () => { - dispose(); - }; - }, []); - return ( ( className={clsx(styles.card, className)} data-testid="doc-card" data-doc-id={meta.id} - data-visible={visible} {...attrs} > - {visible && ( - <> -
    -

    {title}

    - - } - /> -
    -
    - - - - - } - pageId={meta.id} - emptyFallback={
    Empty
    } - /> -
    - {showTags ? : null} - - )} +
    +

    {title}

    + + } + /> +
    +
    + + + + + } + pageId={meta.id} + emptyFallback={
    Empty
    } + /> +
    + {showTags ? : null}
    ); } diff --git a/packages/frontend/core/src/mobile/components/page/index.tsx b/packages/frontend/core/src/mobile/components/page/index.tsx new file mode 100644 index 0000000000..445b37e21a --- /dev/null +++ b/packages/frontend/core/src/mobile/components/page/index.tsx @@ -0,0 +1,31 @@ +import { type HTMLAttributes, type ReactNode, useEffect } from 'react'; + +import { AppTabs } from '../app-tabs'; +import * as styles from './styles.css'; + +interface PageProps extends HTMLAttributes { + tab?: boolean; + header?: ReactNode; +} + +/** + * A Page is a full-screen container that will not scroll on document. + */ +export const Page = ({ children, tab = true, header, ...attrs }: PageProps) => { + // disable scroll on body + useEffect(() => { + const prevOverflowY = document.body.style.overflowY; + document.body.style.overflowY = 'hidden'; + return () => { + document.body.style.overflowY = prevOverflowY; + }; + }, []); + + return ( +
    + {header} + {children} + {tab ? : null} +
    + ); +}; diff --git a/packages/frontend/core/src/mobile/components/page/styles.css.ts b/packages/frontend/core/src/mobile/components/page/styles.css.ts new file mode 100644 index 0000000000..9a10f32080 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/page/styles.css.ts @@ -0,0 +1,13 @@ +import { style } from '@vanilla-extract/css'; + +export const page = style({ + width: '100dvw', + height: '100dvh', + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + + selectors: { + '&[data-tab="true"]': {}, + }, +}); diff --git a/packages/frontend/core/src/mobile/pages/workspace/all.tsx b/packages/frontend/core/src/mobile/pages/workspace/all.tsx index d2c2f15122..8b12154369 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/all.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/all.tsx @@ -1,18 +1,14 @@ -import { SafeArea, useThemeColorV2 } from '@affine/component'; +import { useThemeColorV2 } from '@affine/component'; -import { AppTabs } from '../../components'; +import { Page } from '../../components/page'; import { AllDocList, AllDocsHeader, AllDocsMenu } from '../../views'; export const Component = () => { useThemeColorV2('layer/background/mobile/primary'); return ( - <> - } /> - - - - - + } />} tab> + + ); }; diff --git a/packages/frontend/core/src/mobile/pages/workspace/collection/detail.tsx b/packages/frontend/core/src/mobile/pages/workspace/collection/detail.tsx index 6add099d7a..814954ee3c 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/collection/detail.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/collection/detail.tsx @@ -10,7 +10,6 @@ import { import { useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { AppTabs } from '../../../components'; import { CollectionDetail } from '../../../views'; export const Component = () => { @@ -68,10 +67,5 @@ export const Component = () => { return null; } - return ( - <> - - - - ); + return ; }; diff --git a/packages/frontend/core/src/mobile/styles/mobile.css.ts b/packages/frontend/core/src/mobile/styles/mobile.css.ts index 0cb7e5d47e..9bf8ff8300 100644 --- a/packages/frontend/core/src/mobile/styles/mobile.css.ts +++ b/packages/frontend/core/src/mobile/styles/mobile.css.ts @@ -3,11 +3,13 @@ import { createVar, globalStyle } from '@vanilla-extract/css'; export const globalVars = { appTabHeight: createVar('appTabHeight'), + appTabSafeArea: createVar('appTabSafeArea'), }; globalStyle(':root', { vars: { [globalVars.appTabHeight]: BUILD_CONFIG.isIOS ? '49px' : '62px', + [globalVars.appTabSafeArea]: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom))`, }, userSelect: 'none', WebkitUserSelect: 'none', @@ -18,17 +20,17 @@ globalStyle('body', { minHeight: '100dvh', overflowY: 'unset', }); -globalStyle('body:has(#app-tabs)', { - paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom))`, +globalStyle('body:has(> #app-tabs)', { + paddingBottom: globalVars.appTabSafeArea, }); globalStyle('body:has(#app-tabs) affine-keyboard-toolbar[data-shrink="true"]', { - paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom))`, + paddingBottom: globalVars.appTabSafeArea, }); globalStyle('body:has(#app-tabs) affine-keyboard-tool-panel', { paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom) + 8px)`, }); globalStyle('body:has(#app-tabs) edgeless-toolbar-widget', { - bottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom))`, + bottom: globalVars.appTabSafeArea, }); globalStyle('html', { height: '100dvh', diff --git a/packages/frontend/core/src/mobile/views/all-docs/collection/detail.tsx b/packages/frontend/core/src/mobile/views/all-docs/collection/detail.tsx index 1c07feb55a..56fcb03ad6 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/collection/detail.tsx +++ b/packages/frontend/core/src/mobile/views/all-docs/collection/detail.tsx @@ -1,7 +1,8 @@ import { IconButton, MobileMenu } from '@affine/component'; import { EmptyCollectionDetail } from '@affine/core/components/affine/empty'; import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection'; -import { PageHeader } from '@affine/core/mobile/components'; +import { AppTabs, PageHeader } from '@affine/core/mobile/components'; +import { Page } from '@affine/core/mobile/components/page'; import type { Collection } from '@affine/env/filter'; import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc'; @@ -42,14 +43,14 @@ export const CollectionDetail = ({ <> + ); } return ( - <> - + }> - + ); }; diff --git a/packages/frontend/core/src/mobile/views/all-docs/doc/list.tsx b/packages/frontend/core/src/mobile/views/all-docs/doc/list.tsx index 2ff82558bc..f8e37cdccb 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/doc/list.tsx +++ b/packages/frontend/core/src/mobile/views/all-docs/doc/list.tsx @@ -1,13 +1,10 @@ import { EmptyDocs } from '@affine/core/components/affine/empty'; import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta'; 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 type { Tag } from '@affine/core/modules/tag'; import type { Collection, Filter } from '@affine/env/filter'; import type { DocMeta } from '@blocksuite/affine/store'; @@ -19,7 +16,7 @@ import { useMemo } from 'react'; import * as styles from './list.css'; import { MasonryDocs } from './masonry'; -const DocGroup = ({ group }: { group: ItemGroupProps }) => { +export const DocGroup = ({ group }: { group: ItemGroupProps }) => { const [properties] = useAllDocDisplayProperties(); const showTags = properties.displayProperties.tags; @@ -53,6 +50,7 @@ export const AllDocList = ({ tag, filters = [], }: AllDocListProps) => { + const [properties] = useAllDocDisplayProperties(); const workspace = useService(WorkspaceService).workspace; const allPageMetas = useBlockSuiteDocMeta(workspace.docCollection); @@ -72,22 +70,29 @@ export const AllDocList = ({ return filteredPageMetas; }, [filteredPageMetas, tag, tagPageIds]); - const groupDefs = - usePageItemGroupDefinitions() as ItemGroupDefinition[]; + // const groupDefs = + // usePageItemGroupDefinitions() as ItemGroupDefinition[]; - const groups = useMemo(() => { - return itemsToItemGroups(finalPageMetas ?? [], groupDefs); - }, [finalPageMetas, groupDefs]); + // const groups = useMemo(() => { + // return itemsToItemGroups(finalPageMetas ?? [], groupDefs); + // }, [finalPageMetas, groupDefs]); - if (!groups.length) { + if (!finalPageMetas.length) { return ; } + // return ( + //
    + // {groups.map(group => ( + // + // ))} + //
    + // ); + return ( -
    - {groups.map(group => ( - - ))} -
    + ); }; diff --git a/packages/frontend/core/src/mobile/views/all-docs/doc/masonry.tsx b/packages/frontend/core/src/mobile/views/all-docs/doc/masonry.tsx index 4e887a9794..991ca7832c 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/doc/masonry.tsx +++ b/packages/frontend/core/src/mobile/views/all-docs/doc/masonry.tsx @@ -1,31 +1,11 @@ -import { useGlobalEvent } from '@affine/core/mobile/hooks/use-global-events'; +import { Masonry } from '@affine/component'; import type { DocMeta } from '@blocksuite/affine/store'; -import { useCallback, useMemo, useState } from 'react'; import { calcRowsById, DocCard } from '../../../components'; -import * as styles from './masonry.css'; -const calcColumnCount = () => { - const maxCardWidth = 220; - const windowWidth = window.innerWidth; - const newColumnCount = Math.floor( - (windowWidth - styles.paddingX * 2 - styles.columnGap) / maxCardWidth - ); - return Math.max(newColumnCount, 2); -}; - -const calcColumns = (items: DocMeta[], length: number) => { - const columns = Array.from({ length }, () => [] as DocMeta[]); - const heights = Array.from({ length }, () => 0); - - items.forEach(item => { - const itemHeight = calcRowsById(item.id); - const minHeightIndex = heights.indexOf(Math.min(...heights)); - heights[minHeightIndex] += itemHeight; - columns[minHeightIndex].push(item); - }); - - return columns; +const fullStyle = { + width: '100%', + height: '100%', }; export const MasonryDocs = ({ @@ -35,32 +15,25 @@ export const MasonryDocs = ({ items: DocMeta[]; showTags?: boolean; }) => { - const [columnCount, setColumnCount] = useState(calcColumnCount); - - const updateColumnCount = useCallback(() => { - setColumnCount(calcColumnCount()); - }, []); - useGlobalEvent('resize', updateColumnCount); - - const columns = useMemo( - () => calcColumns(items, columnCount), - [items, columnCount] - ); - return ( -
    - {columns.map((col, index) => ( -
    - {col.map(item => ( - - ))} -
    + + {items.map(item => ( + + + ))} -
    + ); }; diff --git a/packages/frontend/core/src/mobile/views/all-docs/tag/detail.tsx b/packages/frontend/core/src/mobile/views/all-docs/tag/detail.tsx index 1e040a536f..f11b68292a 100644 --- a/packages/frontend/core/src/mobile/views/all-docs/tag/detail.tsx +++ b/packages/frontend/core/src/mobile/views/all-docs/tag/detail.tsx @@ -1,15 +1,13 @@ +import { Page } from '@affine/core/mobile/components/page'; import type { Tag } from '@affine/core/modules/tag'; -import { AppTabs } from '../../../components'; import { AllDocList } from '../doc'; import { TagDetailHeader } from './detail-header'; export const TagDetail = ({ tag }: { tag: Tag }) => { return ( - <> - + } tab> - - + ); };