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;

View File

@@ -49,6 +49,9 @@ export const createDocExplorerContext = (
selectedDocIds$: new LiveData<string[]>([]),
prevCheckAnchorId$: new LiveData<string | null>(null),
displayPreference$: displayPreference$,
showDragHandle$: displayPreference$.selector(
displayPreference => displayPreference.showDragHandle
),
view$: displayPreference$.selector(
displayPreference => displayPreference.view
),
@@ -85,9 +88,6 @@ export const createDocExplorerContext = (
showMoreOperation$: displayPreference$.selector(
displayPreference => displayPreference.showMoreOperation
),
showDragHandle$: displayPreference$.selector(
displayPreference => displayPreference.showDragHandle
),
quickDeletePermanently$: displayPreference$.selector(
displayPreference => displayPreference.quickDeletePermanently
),

View File

@@ -1,4 +1,4 @@
import { MenuItem } from '@affine/component';
import { Divider, MenuItem } from '@affine/component';
import type { GroupByParams } from '@affine/core/modules/collection-rules/types';
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
import { useI18n } from '@affine/i18n';
@@ -48,8 +48,9 @@ export const GroupByList = ({
onChange,
}: {
groupBy?: GroupByParams;
onChange?: (next: GroupByParams) => void;
onChange?: (next: GroupByParams | undefined) => void;
}) => {
const t = useI18n();
const workspacePropertyService = useService(WorkspacePropertyService);
const propertyList = useLiveData(workspacePropertyService.sortedProperties$);
const explorerPropertyList = useMemo(() => {
@@ -66,6 +67,10 @@ export const GroupByList = ({
onChange={onChange}
/>
))}
<Divider size="thinner" style={{ margin: '4px 0' }} />
<MenuItem onClick={() => onChange?.(undefined)}>
{t['com.affine.explorer.display-menu.grouping.remove']()}
</MenuItem>
</>
);
};

View File

@@ -32,7 +32,7 @@ const ExplorerDisplayMenu = ({
const t = useI18n();
const handleGroupByChange = useCallback(
(groupBy: GroupByParams) => {
(groupBy: GroupByParams | undefined) => {
onDisplayPreferenceChange({ ...displayPreference, groupBy });
},
[displayPreference, onDisplayPreferenceChange]

View File

@@ -159,6 +159,14 @@ export const listHide560 = style({
export const cardViewRoot = style({
vars: {
'--ring-color': 'transparent',
'--light-shadow':
'0px 0px 0px 1px var(--ring-color), 0px 2px 3px rgba(0,0,0,.05)',
'--dark-shadow':
'0px 0px 0px 1px var(--ring-color), 0px 2px 3px rgba(0,0,0,.05)',
'--light-shadow-hover':
'0px 0px 0px 1px var(--ring-color), 0px 4px 6px rgba(0,0,0,.1)',
'--dark-shadow-hover':
'0px 0px 0px 1px var(--ring-color), 0px 4px 6px rgba(0,0,0,.1)',
},
width: '100%',
height: '100%',
@@ -170,14 +178,30 @@ export const cardViewRoot = style({
backgroundColor: cssVarV2.layer.background.mobile.secondary,
border: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
// TODO: use variable
boxShadow: '0px 0px 0px 1px var(--ring-color),0px 0px 3px rgba(0,0,0,.05)',
boxShadow: '0px 0px 0px 1px var(--ring-color), 0px 0px 3px rgba(0,0,0,.05)',
overflow: 'hidden',
transition: 'box-shadow 0.23s ease, border-color 0.23s ease',
selectors: {
[`${root}[data-selected="true"] &`]: {
vars: {
'--ring-color': cssVarV2.layer.insideBorder.primaryBorder,
},
},
'&:hover': {
borderColor: cssVarV2.pagelist.hoverBorder,
},
'[data-theme="light"] &': {
boxShadow: 'var(--light-shadow)',
},
'[data-theme="light"] &:hover': {
boxShadow: 'var(--light-shadow-hover)',
},
'[data-theme="dark"] &': {
boxShadow: 'var(--dark-shadow)',
},
'[data-theme="dark"] &:hover': {
boxShadow: 'var(--dark-shadow-hover)',
},
},
});
export const cardViewHeader = style({
@@ -224,3 +248,17 @@ export const cardViewCheckbox = style({
color: cssVarV2.icon.primary,
pointerEvents: 'none',
});
export const cardDragHandle = style([
dragHandle,
{
left: -4,
top: 0,
transform: 'translateX(-100%)',
opacity: 0,
selectors: {
[`${cardViewRoot}:hover &`]: {
opacity: 1,
},
},
},
]);

View File

@@ -1,9 +1,9 @@
import {
Checkbox,
DragHandle as DragHandleIcon,
Skeleton,
Tooltip,
useDraggable,
Wrapper,
} from '@affine/component';
import { DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
@@ -23,7 +23,6 @@ import {
type SVGProps,
useCallback,
useContext,
useState,
} from 'react';
import { PagePreview } from '../../page-list/page-content-preview';
@@ -312,9 +311,6 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
const t = useI18n();
const docsService = useService(DocsService);
const doc = useLiveData(docsService.list.doc$(docId));
const [previewSkeletonWidth] = useState(
Math.round(100 + Math.random() * 100)
);
if (!doc) {
return null;
@@ -334,7 +330,7 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
<DocPreview
id={docId}
className={styles.listPreview}
loading={<Skeleton height={20} width={previewSkeletonWidth} />}
loading={<Wrapper height={20} width={10} />}
/>
</div>
<div className={styles.listSpace} />
@@ -360,19 +356,13 @@ const cardMoreMenuContentOptions = {
sideOffset: 12,
alignOffset: -4,
} as const;
const randomPreviewSkeleton = () => {
return Array.from({ length: Math.floor(Math.random() * 2) + 1 }, (_, i) => ({
id: i,
width: Math.round(30 + Math.random() * 70),
}));
};
export const CardViewDoc = ({ docId }: DocListItemProps) => {
const t = useI18n();
const contextValue = useContext(DocExplorerContext);
const selectMode = useLiveData(contextValue.selectMode$);
const docsService = useService(DocsService);
const doc = useLiveData(docsService.list.doc$(docId));
const [previewSkeleton] = useState(randomPreviewSkeleton);
if (!doc) {
return null;
@@ -380,6 +370,7 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
return (
<li className={styles.cardViewRoot}>
<DragHandle id={docId} className={styles.cardDragHandle} />
<header className={styles.cardViewHeader}>
<DocIcon id={docId} className={styles.cardViewIcon} />
<DocTitle
@@ -404,17 +395,7 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
/>
)}
</header>
<DocPreview
id={docId}
className={styles.cardPreviewContainer}
loading={
<div>
{previewSkeleton.map(({ id, width }) => (
<Skeleton key={id} height={20} width={`${width}%`} />
))}
</div>
}
/>
<DocPreview id={docId} className={styles.cardPreviewContainer} />
<CardViewProperties docId={docId} />
</li>
);

View File

@@ -1,4 +1,9 @@
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
import {
Masonry,
type MasonryGroup,
type MasonryItem,
useConfirmModal,
} from '@affine/component';
import { DocsService } from '@affine/core/modules/doc';
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
import { Trans, useI18n } from '@affine/i18n';
@@ -69,15 +74,13 @@ const GroupHeader = memo(function GroupHeader({
return header;
});
const calcCardHeightById = (id: string, base = 250, scale = 10) => {
const ratios = [1.26, 1.304, 1.13, 1.391, 1.521];
const calcCardRatioById = (id: string) => {
if (!id) {
return base;
return ratios[0];
}
const max = 5;
const min = 1;
const code = id.charCodeAt(0);
const value = Math.floor((code % (max - min)) + min);
return base + value * scale;
return ratios[code % ratios.length];
};
export const DocListItemComponent = memo(function DocListItemComponent({
@@ -94,16 +97,12 @@ export const DocsExplorer = ({
className,
disableMultiDelete,
masonryItemWidthMin,
heightBase,
heightScale,
onRestore,
onDelete,
}: {
className?: string;
disableMultiDelete?: boolean;
masonryItemWidthMin?: number;
heightBase?: number;
heightScale?: number;
onRestore?: (ids: string[]) => void;
/** Override the default delete action */
onDelete?: (ids: string[]) => void;
@@ -129,23 +128,23 @@ export const DocsExplorer = ({
height: groupBy ? 24 : 0,
className: styles.groupHeader,
items: group.items.map((docId: string) => {
if (view === 'list') {
return {
id: docId,
Component: DocListItemComponent,
height: 42,
} satisfies MasonryItem;
}
return {
id: docId,
Component: DocListItemComponent,
height:
view === 'list'
? 42
: view === 'grid'
? 280
: calcCardHeightById(docId, heightBase, heightScale),
'data-view': view,
className: styles.docItem,
};
ratio: view === 'grid' ? ratios[0] : calcCardRatioById(docId),
} satisfies MasonryItem;
}),
} satisfies MasonryGroup;
});
return items;
}, [groupBy, groups, heightBase, heightScale, view]);
}, [groupBy, groups, view]);
const handleCloseFloatingToolbar = useCallback(() => {
contextValue.selectMode$?.next(false);
@@ -222,13 +221,18 @@ export const DocsExplorer = ({
};
}, [contextValue]);
const responsivePaddingX = useCallback(
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
[]
);
return (
<>
<Masonry
className={className}
items={masonryItems}
gapY={12}
gapX={12}
gapY={BUILD_CONFIG.isMobileEdition ? 12 : view === 'list' ? 12 : 24}
gapX={BUILD_CONFIG.isMobileEdition ? 12 : 24}
groupsGap={12}
groupHeaderGapWithItems={12}
columns={view === 'list' ? 1 : undefined}
@@ -237,10 +241,8 @@ export const DocsExplorer = ({
itemWidth={'stretch'}
virtualScroll
collapsedGroups={collapsedGroups}
paddingX={useCallback(
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
[]
)}
paddingY={BUILD_CONFIG.isMobileEdition ? 12 : 0}
paddingX={BUILD_CONFIG.isMobileEdition ? 16 : responsivePaddingX}
/>
{!disableMultiDelete || onRestore ? (
<ListFloatingToolbar

View File

@@ -43,6 +43,9 @@ export const DocGroupHeader = ({
}, [groupId, contextValue]);
const handleSelectAll = useCallback(() => {
if (!contextValue.selectMode$?.value) {
contextValue.selectMode$?.next(true);
}
const prev = contextValue.selectedDocIds$.value;
if (isGroupAllSelected) {
contextValue.selectedDocIds$.next(

View File

@@ -2,7 +2,12 @@ import { IconButton, Menu, MenuItem, MenuSeparator } from '@affine/component';
import type { FilterParams } from '@affine/core/modules/collection-rules';
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
import { useI18n } from '@affine/i18n';
import { ArrowLeftBigIcon, FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
import {
ArrowLeftBigIcon,
CloudWorkspaceIcon,
FavoriteIcon,
PlusIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react';
@@ -57,7 +62,9 @@ export const AddFilterMenu = ({
<span className={styles.filterTypeItemName}>{t['Favorited']()}</span>
</MenuItem>
<MenuItem
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
prefixIcon={
<CloudWorkspaceIcon className={styles.filterTypeItemIcon} />
}
key={'shared'}
onClick={() => {
onAdd({

View File

@@ -1,5 +1,9 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const menuContent = style({
backgroundColor: cssVar('backgroundOverlayPanelColor'),
});
export const button = style({
backgroundColor: cssVarV2.button.secondary,
});

View File

@@ -6,7 +6,7 @@ import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons/rc';
import type { MouseEvent, PropsWithChildren } from 'react';
import { useCallback, useState } from 'react';
import { menuContent } from './new-page-button.css';
import * as styles from './new-page-button.css';
type NewPageButtonProps = {
createNewDoc: (e?: MouseEvent) => void;
@@ -120,7 +120,7 @@ export const NewPageButton = ({
open,
}}
contentOptions={{
className: menuContent,
className: styles.menuContent,
align: 'end',
hideWhenDetached: true,
onInteractOutside: useCallback(() => {
@@ -133,6 +133,7 @@ export const NewPageButton = ({
onClick={handleCreateNewDoc}
onAuxClick={handleCreateNewPage}
onClickDropDown={useCallback(() => setOpen(open => !open), [])}
className={styles.button}
>
{children}
</DropdownButton>

View File

@@ -13,6 +13,19 @@ import type { GroupHeaderProps } from '../explorer/types';
import { MemberSelectorInline } from '../member-selector';
import * as styles from './created-updated-by.css';
const CreatedUsernameTip = ({ userName }: { userName: string }) => {
const i18n = useI18n();
return i18n.t('com.affine.page-properties.property.createdBy.tip', {
userName,
});
};
const UpdatedUsernameTip = ({ userName }: { userName: string }) => {
const i18n = useI18n();
return i18n.t('com.affine.page-properties.property.updatedBy.tip', {
userName,
});
};
const CreatedByUpdatedByAvatar = (props: {
type: 'CreatedBy' | 'UpdatedBy';
doc: DocRecord;
@@ -33,6 +46,9 @@ const CreatedByUpdatedByAvatar = (props: {
id={userId}
size={props.size}
showName={props.showName}
tooltip={
props.type === 'CreatedBy' ? CreatedUsernameTip : UpdatedUsernameTip
}
/>
</div>
);

View File

@@ -33,7 +33,7 @@ export const body = style({
export const scrollArea = style({
height: 0,
flex: 1,
paddingTop: '24px',
paddingTop: '12px',
});
// group
@@ -43,7 +43,7 @@ export const pinnedCollection = style({
flexDirection: 'column',
gap: 8,
padding: '0 24px',
paddingTop: '24px',
paddingTop: '12px',
'@container': {
'docs-body (width <= 500px)': {
padding: '0 20px',

View File

@@ -68,11 +68,7 @@ const AllDocs = () => {
return (
<DocExplorerContext.Provider value={explorerContextValue}>
<DocsExplorer
masonryItemWidthMin={150}
heightBase={180}
heightScale={12}
/>
<DocsExplorer masonryItemWidthMin={150} />
</DocExplorerContext.Provider>
);
};

View File

@@ -69,11 +69,7 @@ const CollectionDocs = ({ collection }: { collection: Collection }) => {
return (
<DocExplorerContext.Provider value={explorerContextValue}>
<DocsExplorer
masonryItemWidthMin={150}
heightBase={180}
heightScale={12}
/>
<DocsExplorer masonryItemWidthMin={150} />
</DocExplorerContext.Provider>
);
};

View File

@@ -75,11 +75,7 @@ const TagDocs = ({ tag }: { tag: Tag }) => {
return (
<DocExplorerContext.Provider value={explorerContextValue}>
<DocsExplorer
masonryItemWidthMin={150}
heightBase={180}
heightScale={12}
/>
<DocsExplorer masonryItemWidthMin={150} />
</DocExplorerContext.Provider>
);
};

View File

@@ -1,8 +1,8 @@
import { Avatar } from '@affine/component';
import { Avatar, Tooltip } from '@affine/component';
import { useCurrentServerService } from '@affine/core/components/providers/current-server-scope';
import { useI18n } from '@affine/i18n';
import { useLiveData } from '@toeverything/infra';
import { useLayoutEffect, useMemo } from 'react';
import { type ComponentType, useLayoutEffect, useMemo } from 'react';
import { PublicUserService } from '../services/public-user';
import * as styles from './public-user.css';
@@ -11,10 +11,12 @@ export const PublicUserLabel = ({
id,
size = 20,
showName = true,
tooltip: NameTip,
}: {
id: string;
size?: number;
showName?: boolean;
tooltip?: ComponentType<{ userName: string }>;
}) => {
const serverService = useCurrentServerService();
const publicUser = useMemo(() => {
@@ -50,15 +52,23 @@ export const PublicUserLabel = ({
}
return (
<span className={styles.publicUserLabel}>
<Avatar
url={user?.avatar}
name={user?.name ?? ''}
size={size}
className={styles.publicUserLabelAvatar}
data-show-name={showName}
/>
{showName && user?.name}
</span>
<Tooltip
content={
NameTip ? (
<NameTip userName={user?.name || t['Unknown User']()} />
) : null
}
>
<span className={styles.publicUserLabel}>
<Avatar
url={user?.avatar}
name={user?.name ?? ''}
size={size}
className={styles.publicUserLabelAvatar}
data-show-name={showName}
/>
{showName && user?.name}
</span>
</Tooltip>
);
};

View File

@@ -2189,6 +2189,10 @@ export function useAFFiNEI18N(): {
* `Grouping`
*/
["com.affine.explorer.display-menu.grouping"](): string;
/**
* `Remove group`
*/
["com.affine.explorer.display-menu.grouping.remove"](): string;
/**
* `Ordering`
*/
@@ -3141,6 +3145,18 @@ export function useAFFiNEI18N(): {
* `Mark this doc as a template, which can be used to create new docs.`
*/
["com.affine.page-properties.property.template.tooltips"](): string;
/**
* `Created by {{userName}}`
*/
["com.affine.page-properties.property.createdBy.tip"](options: {
readonly userName: string;
}): string;
/**
* `Last edited by {{userName}}`
*/
["com.affine.page-properties.property.updatedBy.tip"](options: {
readonly userName: string;
}): string;
/**
* `Properties`
*/

View File

@@ -546,6 +546,7 @@
"com.affine.filterList.button.add": "Add filter",
"com.affine.explorer.display-menu.button": "Display",
"com.affine.explorer.display-menu.grouping": "Grouping",
"com.affine.explorer.display-menu.grouping.remove": "Remove group",
"com.affine.explorer.display-menu.ordering": "Ordering",
"com.affine.header.mode-switch.page": "View in Page mode",
"com.affine.header.mode-switch.edgeless": "View in Edgeless Canvas",
@@ -780,6 +781,8 @@
"com.affine.page-properties.property.edgelessTheme.tooltips": "Select the doc theme from Light, Dark, or System. Useful for precise control over content viewing style.",
"com.affine.page-properties.property.pageWidth.tooltips": "Control the width of this page to fit content display needs.",
"com.affine.page-properties.property.template.tooltips": "Mark this doc as a template, which can be used to create new docs.",
"com.affine.page-properties.property.createdBy.tip": "Created by {{userName}}",
"com.affine.page-properties.property.updatedBy.tip": "Last edited by {{userName}}",
"com.affine.propertySidebar.property-list.section": "Properties",
"com.affine.propertySidebar.add-more.section": "Add more properties",
"com.affine.page-properties.settings.title": "customize properties",