feat(core): card view drag handle for doc explorer (#12431)

close AF-2624, AF-2628, AF-2581

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a draggable handle to document cards in the explorer, visible on hover in card view.
  - Added an option to remove grouping in the display menu.
  - Added contextual tooltips for user avatars indicating creation or last update.
  - Enabled optional tooltips on public user labels.
  - Extended dropdown buttons to accept custom styling classes.
  - Added a new masonry story showcasing item heights determined by ratios.

- **Style**
  - Enhanced drag handle appearance and visibility for card view items.
  - Replaced static shadows with theme-aware, smoothly transitioning shadows on card items.
  - Adjusted spacing between items in the document explorer for improved layout, with increased horizontal and (in card view) vertical gaps.
  - Reduced top padding in workspace page styles.
  - Added new button background style for secondary buttons.

- **Bug Fixes**
  - Removed duplicate internal property declarations to eliminate redundancy.

- **Refactor**
  - Simplified layout props by removing fixed height parameters in multiple components.
  - Updated masonry layout logic to support ratio-based item sizing alongside fixed heights.
  - Removed randomized skeleton loading placeholders, replacing them with fixed or no placeholders.
  - Refined masonry component typings and scrollbar placement for improved styling and layout.
  - Improved selection logic to activate selection mode when selecting all documents.

- **Localization**
  - Added new translation keys for grouping removal and user attribution tooltips.
  - Updated English locale with new strings for "Remove group" and user-created/updated tooltips.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
CatsJuice
2025-05-26 03:17:18 +00:00
parent 7d3b7a8555
commit 20af4c35ee
24 changed files with 281 additions and 142 deletions

View File

@@ -1,4 +1,5 @@
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { forwardRef } from 'react';
@@ -12,28 +13,33 @@ type DropdownButtonProps = {
export const DropdownButton = forwardRef<
HTMLButtonElement,
DropdownButtonProps
>(({ onClickDropDown, children, size = 'default', ...props }, ref) => {
const handleClickDropDown: MouseEventHandler<HTMLElement> = e => {
e.stopPropagation();
onClickDropDown?.(e);
};
return (
<button
ref={ref}
data-size={size}
className={styles.dropdownBtn}
{...props}
>
<span>{children}</span>
<span className={styles.divider} />
<span className={styles.dropdownWrapper} onClick={handleClickDropDown}>
<ArrowDownSmallIcon
className={styles.dropdownIcon}
width={16}
height={16}
/>
</span>
</button>
);
});
>(
(
{ onClickDropDown, children, size = 'default', className, ...props },
ref
) => {
const handleClickDropDown: MouseEventHandler<HTMLElement> = e => {
e.stopPropagation();
onClickDropDown?.(e);
};
return (
<button
ref={ref}
data-size={size}
className={clsx(styles.dropdownBtn, className)}
{...props}
>
<span>{children}</span>
<span className={styles.divider} />
<span className={styles.dropdownWrapper} onClick={handleClickDropDown}>
<ArrowDownSmallIcon
className={styles.dropdownIcon}
width={16}
height={16}
/>
</span>
</button>
);
}
);
DropdownButton.displayName = 'DropdownButton';

View File

@@ -279,3 +279,34 @@ export const MultiViewTransition = () => {
</div>
);
};
const availableRatios = [0.8, 1.2, 1.4, 1.5, 1.6, 1.7, 1.8];
const ratioItems = Array.from({ length: 10000 }, (_, i) => {
const ratio =
availableRatios[Math.floor(Math.random() * availableRatios.length)];
return {
id: i.toString(),
ratio,
children: (
<Card>
{i} <br /> ratio: {ratio}
</Card>
),
};
});
export const HeightByRatio = () => {
return (
<ResizePanel width={800} height={600}>
<Masonry
gapX={10}
gapY={10}
style={{ width: '100%', height: '100%' }}
paddingX={12}
paddingY={12}
items={ratioItems}
virtualScroll
itemWidthMin={120}
/>
</ResizePanel>
);
};

View File

@@ -286,7 +286,6 @@ export const Masonry = ({
})}
<div data-masonry-placeholder style={{ height }} />
</Scrollable.Viewport>
<Scrollable.Scrollbar />
{stickyGroup ? (
<div
className={clsx(styles.stickyGroupHeader, stickyGroup.className)}
@@ -310,16 +309,16 @@ export const Masonry = ({
)}
</div>
) : null}
<Scrollable.Scrollbar className={styles.scrollbar} />
</Scrollable.Root>
);
};
interface MasonryItemProps
extends MasonryItem,
Omit<React.HTMLAttributes<HTMLDivElement>, 'id' | 'height'> {
locateMode?: 'transform' | 'leftTop' | 'transform3d';
xywh?: MasonryItemXYWH;
}
type MasonryItemProps = MasonryItem &
Omit<React.HTMLAttributes<HTMLDivElement>, 'id' | 'height'> & {
locateMode?: 'transform' | 'leftTop' | 'transform3d';
xywh?: MasonryItemXYWH;
};
const MasonryGroupHeader = memo(function MasonryGroupHeader({
id,
@@ -330,7 +329,6 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({
groupId,
itemCount,
collapsed,
height,
paddingX,
...props
}: Omit<MasonryItemProps, 'Component'> & {
@@ -356,7 +354,6 @@ const MasonryGroupHeader = memo(function MasonryGroupHeader({
return (
<MasonryItem
id={id}
height={height}
style={{
padding: `0 ${paddingX}px`,
height: '100%',
@@ -404,7 +401,7 @@ const MasonryItem = memo(function MasonryItem({
className,
style: styleProp,
...props
}: MasonryItemProps) {
}: Omit<MasonryItemProps, 'height' | 'ratio'>) {
const style = useMemo(() => {
if (!xywh) return { display: 'none' };

View File

@@ -20,6 +20,9 @@ export const stickyGroupHeader = style({
top: 0,
width: '100%',
});
export const scrollbar = style({
zIndex: 1,
});
export const item = style({
position: 'absolute',

View File

@@ -1,8 +1,7 @@
export interface MasonryItem extends React.HTMLAttributes<HTMLDivElement> {
export type MasonryItem = React.HTMLAttributes<HTMLDivElement> & {
id: string;
height: number;
Component?: React.ComponentType<{ groupId: string; itemId: string }>;
}
} & ({ height: number } | { ratio: number });
export interface MasonryGroup extends React.HTMLAttributes<HTMLDivElement> {
id: string;

View File

@@ -82,6 +82,7 @@ export const calcLayout = (
groups.forEach((group, index) => {
const heightStack = Array.from({ length: columns }, () => 0);
const ratioStack = Array.from({ length: columns }, () => 0);
if (index !== 0) {
finalHeight += groupsGap;
}
@@ -101,24 +102,52 @@ export const calcLayout = (
return;
}
finalHeight += groupHeaderLayout.h + groupHeaderGapWithItems;
finalHeight +=
groupHeaderLayout.h +
// if group header is empty, don't add gap
(groupHeaderLayout.h > 0 ? groupHeaderGapWithItems : 0);
// calculate group items
group.items.forEach(item => {
const itemId = group.id ? `${group.id}:${item.id}` : item.id;
const minHeight = Math.min(...heightStack);
const minHeightIndex = heightStack.indexOf(minHeight);
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
const x = minHeightIndex * (width + gapX) + paddingX;
const y = finalHeight + minHeight + hasGap;
const ratioMode = 'ratio' in item;
const height = ratioMode ? item.ratio * width : item.height;
heightStack[minHeightIndex] += item.height + hasGap;
layout.set(itemId, {
type: 'item',
x,
y,
w: width,
h: item.height,
});
if (ratioMode) {
const minRatio = Math.min(...ratioStack);
const minRatioIndex = ratioStack.indexOf(minRatio);
const minHeight = heightStack[minRatioIndex];
const hasGap = heightStack[minRatioIndex] ? gapY : 0;
const x = minRatioIndex * (width + gapX) + paddingX;
const y = finalHeight + minHeight + hasGap;
ratioStack[minRatioIndex] += item.ratio * 10000;
heightStack[minRatioIndex] += height + hasGap;
layout.set(itemId, {
type: 'item',
x,
y,
w: width,
h: height,
});
} else {
const minHeight = Math.min(...heightStack);
const minHeightIndex = heightStack.indexOf(minHeight);
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
const x = minHeightIndex * (width + gapX) + paddingX;
const y = finalHeight + minHeight + hasGap;
const ratio = height / width;
heightStack[minHeightIndex] += height + hasGap;
ratioStack[minHeightIndex] += ratio * 10000;
layout.set(itemId, {
type: 'item',
x,
y,
w: width,
h: height,
});
}
});
const groupHeight = Math.max(...heightStack) + paddingY;