mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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' };
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export const stickyGroupHeader = style({
|
||||
top: 0,
|
||||
width: '100%',
|
||||
});
|
||||
export const scrollbar = style({
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const item = style({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user