diff --git a/packages/frontend/component/src/ui/masonry/masonry.stories.tsx b/packages/frontend/component/src/ui/masonry/masonry.stories.tsx
index 76cc54267b..0561f4b868 100644
--- a/packages/frontend/component/src/ui/masonry/masonry.stories.tsx
+++ b/packages/frontend/component/src/ui/masonry/masonry.stories.tsx
@@ -1,3 +1,6 @@
+import { memo, useCallback, useMemo, useState } from 'react';
+
+import { RadioGroup } from '../radio';
import { ResizePanel } from '../resize-panel/resize-panel';
import { Masonry } from './masonry';
@@ -5,20 +8,43 @@ export default {
title: 'UI/Masonry',
};
-const Card = ({ children }: { children: React.ReactNode }) => {
+const Card = ({
+ children,
+ listView,
+}: {
+ children: React.ReactNode;
+ listView?: boolean;
+}) => {
return (
{children}
+ {listView && (
+
+ )}
);
};
@@ -78,3 +104,178 @@ export const CustomTransition = () => {
);
};
+
+const groups = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(letter => {
+ return {
+ id: letter,
+ height: 20,
+ children: Group header: {letter}
,
+ items: Array.from({ length: 100 }, (_, i) => {
+ return {
+ id: i,
+ height: Math.round(100 + Math.random() * 100),
+ children: (
+
+ Group: {letter}
+ Item: {i}
+
+ ),
+ };
+ }),
+ };
+});
+
+export const GroupVirtualScroll = () => {
+ return (
+
+
+
+ );
+};
+
+const GroupHeader = memo(function GroupHeader({
+ groupId,
+ collapsed,
+ itemCount,
+}: {
+ groupId: string;
+ collapsed?: boolean;
+ itemCount: number;
+}) {
+ return (
+
+
+ Group header: {groupId} - {itemCount} items{' '}
+
+ >
+
+
+
+ );
+});
+
+const GroupItem = ({
+ groupId,
+ itemId,
+ view,
+}: {
+ groupId: string;
+ itemId: string;
+ view: 'Masonry' | 'Grid' | 'List';
+}) => {
+ return (
+
+ Group: {groupId}
+ Item: {itemId}
+
+ );
+};
+
+const viewGroups = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(letter => {
+ return {
+ id: letter,
+ height: 40,
+ Component: GroupHeader,
+ style: { transition: 'all 0.4s cubic-bezier(.4,.22,0,.98)' },
+ items: Array.from(
+ { length: Math.round(50 + Math.random() * 50) },
+ (_, i) => {
+ return {
+ id: `${i}`,
+ height: {
+ List: 32,
+ Masonry: Math.round(100 + Math.random() * 100),
+ Grid: 100,
+ },
+ style: { transition: 'all 0.4s cubic-bezier(.4,.22,0,.98)' },
+ };
+ }
+ ),
+ } as const;
+});
+
+export const MultiViewTransition = () => {
+ const [view, setView] = useState<'Masonry' | 'Grid' | 'List'>('List');
+ const [collapsedGroups, setCollapsedGroups] = useState([]);
+
+ const groups = useMemo(() => {
+ return viewGroups.map(({ items, ...g }) => ({
+ ...g,
+ items: items.map(({ height, ...item }) => ({
+ ...item,
+ height: height[view],
+ children: ,
+ })),
+ }));
+ }, [view]);
+
+ const onGroupCollapse = useCallback((groupId: string, collapsed: boolean) => {
+ setCollapsedGroups(prev => {
+ return collapsed ? [...prev, groupId] : prev.filter(id => id !== groupId);
+ });
+ }, []);
+
+ return (
+
+
+ [x * 2, y], [])}
+ >
+
+
+
+ );
+};
diff --git a/packages/frontend/component/src/ui/masonry/masonry.tsx b/packages/frontend/component/src/ui/masonry/masonry.tsx
index cd501e3ec6..751bee05c3 100644
--- a/packages/frontend/component/src/ui/masonry/masonry.tsx
+++ b/packages/frontend/component/src/ui/masonry/masonry.tsx
@@ -1,20 +1,35 @@
import clsx from 'clsx';
+import { debounce } from 'lodash-es';
import throttle from 'lodash-es/throttle';
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ Fragment,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { observeResize } from '../../utils';
import { Scrollable } from '../scrollbar';
import * as styles from './styles.css';
-import type { MasonryItem, MasonryItemXYWH } from './type';
-import { calcColumns, calcLayout, calcSleep } from './utils';
+import type { MasonryGroup, MasonryItem, MasonryItemXYWH } from './type';
+import { calcColumns, calcLayout, calcSleep, calcSticky } from './utils';
export interface MasonryProps extends React.HTMLAttributes {
- items: MasonryItem[];
+ items: MasonryItem[] | MasonryGroup[];
gapX?: number;
gapY?: number;
paddingX?: number;
paddingY?: number;
+
+ groupsGap?: number;
+ groupHeaderGapWithItems?: number;
+ stickyGroupHeader?: boolean;
+ collapsedGroups?: string[];
+ onGroupCollapse?: (groupId: string, collapsed: boolean) => void;
/**
* Specify the width of the item.
* - `number`: The width of the item in pixels.
@@ -29,6 +44,10 @@ export interface MasonryProps extends React.HTMLAttributes {
itemWidthMin?: number;
virtualScroll?: boolean;
locateMode?: 'transform' | 'leftTop' | 'transform3d';
+ /**
+ * Specify the number of columns, will override the calculated
+ */
+ columns?: number;
}
export const Masonry = ({
@@ -42,6 +61,12 @@ export const Masonry = ({
className,
virtualScroll = false,
locateMode = 'leftTop',
+ groupsGap = 0,
+ groupHeaderGapWithItems = 0,
+ stickyGroupHeader = true,
+ collapsedGroups,
+ columns,
+ onGroupCollapse,
...props
}: MasonryProps) => {
const rootRef = useRef(null);
@@ -49,9 +74,31 @@ export const Masonry = ({
const [layoutMap, setLayoutMap] = useState<
Map
>(new Map());
- const [sleepMap, setSleepMap] = useState