mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(core): mobile renderer for explorer (#7942)
This commit is contained in:
@@ -64,3 +64,30 @@ export const collapseIcon = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ------------- mobile -------------
|
||||
export const mobileRoot = style([
|
||||
root,
|
||||
{
|
||||
height: 25,
|
||||
padding: '0 16px',
|
||||
selectors: {
|
||||
'&[data-collapsible="true"]:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
'&[data-collapsible="true"]:active': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const mobileLabel = style([
|
||||
label,
|
||||
{
|
||||
color: cssVarV2('text/primary'),
|
||||
fontSize: 20,
|
||||
lineHeight: '25px',
|
||||
letterSpacing: -0.45,
|
||||
fontWeight: 400,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -9,6 +9,7 @@ export type CategoryDividerProps = PropsWithChildren<
|
||||
label: string;
|
||||
className?: string;
|
||||
collapsed?: boolean;
|
||||
mobile?: boolean;
|
||||
setCollapsed?: (collapsed: boolean) => void;
|
||||
} & {
|
||||
[key: `data-${string}`]: unknown;
|
||||
@@ -22,6 +23,7 @@ export const CategoryDivider = forwardRef(
|
||||
children,
|
||||
className,
|
||||
collapsed,
|
||||
mobile,
|
||||
setCollapsed,
|
||||
...otherProps
|
||||
}: CategoryDividerProps,
|
||||
@@ -31,14 +33,15 @@ export const CategoryDivider = forwardRef(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx([styles.root, className])}
|
||||
className={clsx(mobile ? styles.mobileRoot : styles.root, className)}
|
||||
ref={ref}
|
||||
onClick={() => setCollapsed?.(!collapsed)}
|
||||
data-mobile={mobile}
|
||||
data-collapsed={collapsed}
|
||||
data-collapsible={collapsible}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.label}>
|
||||
<div className={mobile ? styles.mobileLabel : styles.label}>
|
||||
{label}
|
||||
{collapsible ? (
|
||||
<ToggleCollapseIcon
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ExplorerSection } from './entities/explore-section';
|
||||
import { ExplorerService } from './services/explorer';
|
||||
export { ExplorerService } from './services/explorer';
|
||||
export type { CollapsibleSectionName } from './types';
|
||||
export { ExplorerMobileContext } from './views/mobile.context';
|
||||
export { ExplorerCollections } from './views/sections/collections';
|
||||
export { ExplorerFavorites } from './views/sections/favorites';
|
||||
export { ExplorerMigrationFavorites } from './views/sections/migration-favorites';
|
||||
|
||||
@@ -13,3 +13,8 @@ export const header = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// mobile
|
||||
export const mobileContent = style({
|
||||
paddingTop: 8,
|
||||
});
|
||||
|
||||
@@ -7,11 +7,18 @@ import {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import { ExplorerService } from '../../services/explorer';
|
||||
import type { CollapsibleSectionName } from '../../types';
|
||||
import { content, header, root } from './collapsible-section.css';
|
||||
import { ExplorerMobileContext } from '../mobile.context';
|
||||
import {
|
||||
content,
|
||||
header,
|
||||
mobileContent,
|
||||
root,
|
||||
} from './collapsible-section.css';
|
||||
|
||||
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
name: CollapsibleSectionName;
|
||||
@@ -43,6 +50,7 @@ export const CollapsibleSection = ({
|
||||
|
||||
contentClassName,
|
||||
}: CollapsibleSectionProps) => {
|
||||
const mobile = useContext(ExplorerMobileContext);
|
||||
const section = useService(ExplorerService).sections[name];
|
||||
|
||||
const collapsed = useLiveData(section.collapsed$);
|
||||
@@ -62,6 +70,7 @@ export const CollapsibleSection = ({
|
||||
data-testid={testId}
|
||||
>
|
||||
<CategoryDivider
|
||||
mobile={mobile}
|
||||
data-testid={headerTestId}
|
||||
label={title}
|
||||
setCollapsed={setCollapsed}
|
||||
@@ -69,9 +78,11 @@ export const CollapsibleSection = ({
|
||||
ref={headerRef}
|
||||
className={clsx(header, headerClassName)}
|
||||
>
|
||||
{actions}
|
||||
{mobile ? null : actions}
|
||||
</CategoryDivider>
|
||||
<Collapsible.Content className={clsx(content, contentClassName)}>
|
||||
<Collapsible.Content
|
||||
className={clsx(mobile ? mobileContent : content, contentClassName)}
|
||||
>
|
||||
{children}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
/**
|
||||
* To enable mobile manually
|
||||
* > Using `environment.isMobile` directly will affect current web entry on mobile
|
||||
* > So we control it manually for now
|
||||
*/
|
||||
export const ExplorerMobileContext = createContext(false);
|
||||
@@ -36,6 +36,14 @@ export const itemRoot = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const itemMain = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 0,
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
gap: 12,
|
||||
});
|
||||
export const itemRenameAnchor = style({
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
@@ -55,31 +63,26 @@ export const itemContent = style({
|
||||
export const postfix = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
right: '4px',
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
selectors: {
|
||||
[`${itemRoot}:hover &`]: {
|
||||
justifySelf: 'flex-end',
|
||||
position: 'initial',
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
pointerEvents: 'initial',
|
||||
position: 'initial',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const icon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: '20px',
|
||||
});
|
||||
export const emojiIcon = style({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
export const iconContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: cssVar('--affine-font-sm'),
|
||||
fontSize: 20,
|
||||
});
|
||||
export const collapsedIconContainer = style({
|
||||
width: '16px',
|
||||
@@ -103,13 +106,6 @@ export const collapsedIconContainer = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const iconsContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
width: '44px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
export const collapsedIcon = style({
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
selectors: {
|
||||
@@ -182,3 +178,67 @@ export const draggedOverEffect = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ---------- mobile ----------
|
||||
export const mobileItemRoot = style([
|
||||
itemRoot,
|
||||
{
|
||||
padding: '8px',
|
||||
borderRadius: 0,
|
||||
flexDirection: 'row-reverse',
|
||||
gap: 12,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&:active': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
'&[data-active="true"]': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
|
||||
':after': {
|
||||
content: '',
|
||||
width: `calc(100% + ${levelIndent})`,
|
||||
height: 0.5,
|
||||
background: cssVar('borderColor'),
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const mobileItemMain = style([itemMain, {}]);
|
||||
|
||||
export const mobileIconContainer = style([
|
||||
iconContainer,
|
||||
{
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: 24,
|
||||
},
|
||||
]);
|
||||
|
||||
export const mobileCollapsedIconContainer = style([
|
||||
collapsedIconContainer,
|
||||
{
|
||||
fontSize: 16,
|
||||
},
|
||||
]);
|
||||
export const mobileItemContent = style([
|
||||
itemContent,
|
||||
{
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
fontWeight: 400,
|
||||
},
|
||||
]);
|
||||
export const mobileContentContainer = style([
|
||||
contentContainer,
|
||||
{
|
||||
marginTop: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { ExplorerMobileContext } from '../mobile.context';
|
||||
import { ExplorerTreeContext } from './context';
|
||||
import { DropEffect } from './drop-effect';
|
||||
import * as styles from './node.css';
|
||||
@@ -108,6 +109,7 @@ export const ExplorerTreeNode = ({
|
||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||
} & { [key in `data-${string}`]?: any }) => {
|
||||
const mobile = useContext(ExplorerMobileContext);
|
||||
const t = useI18n();
|
||||
const cid = useId();
|
||||
const context = useContext(ExplorerTreeContext);
|
||||
@@ -306,35 +308,74 @@ export const ExplorerTreeNode = ({
|
||||
const content = (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={styles.itemRoot}
|
||||
className={mobile ? styles.mobileItemRoot : styles.itemRoot}
|
||||
data-active={active}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
<div className={styles.iconsContainer}>
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="explorer-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
{emoji ? (
|
||||
<div className={styles.emojiIcon}>{emoji}</div>
|
||||
) : (
|
||||
Icon && (
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="explorer-collapsed-button"
|
||||
className={
|
||||
mobile
|
||||
? styles.mobileCollapsedIconContainer
|
||||
: styles.collapsedIconContainer
|
||||
}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={clsx(mobile ? styles.mobileItemMain : styles.itemMain)}>
|
||||
<div
|
||||
className={mobile ? styles.mobileIconContainer : styles.iconContainer}
|
||||
>
|
||||
{emoji ??
|
||||
(Icon && (
|
||||
<Icon
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={mobile ? styles.mobileItemContent : styles.itemContent}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
{postfix}
|
||||
<div
|
||||
className={styles.postfix}
|
||||
onClick={e => {
|
||||
// prevent jump to page
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{inlineOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
{menuOperations.length > 0 && (
|
||||
<Menu
|
||||
items={menuOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
>
|
||||
<IconButton
|
||||
size="16"
|
||||
data-testid="explorer-tree-node-operation-button"
|
||||
style={{ marginLeft: 4 }}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renameable && renaming && (
|
||||
<RenameModal
|
||||
open
|
||||
@@ -346,37 +387,6 @@ export const ExplorerTreeNode = ({
|
||||
<div className={styles.itemRenameAnchor} />
|
||||
</RenameModal>
|
||||
)}
|
||||
|
||||
<div className={styles.itemContent}>{name}</div>
|
||||
|
||||
{postfix}
|
||||
<div
|
||||
className={styles.postfix}
|
||||
onClick={e => {
|
||||
// prevent jump to page
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{inlineOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
{menuOperations.length > 0 && (
|
||||
<Menu
|
||||
items={menuOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
>
|
||||
<IconButton
|
||||
size="16"
|
||||
data-testid="explorer-tree-node-operation-button"
|
||||
style={{ marginLeft: 4 }}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -391,7 +401,10 @@ export const ExplorerTreeNode = ({
|
||||
{...otherProps}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
|
||||
className={clsx(
|
||||
mobile ? styles.mobileContentContainer : styles.contentContainer,
|
||||
styles.draggedOverEffect
|
||||
)}
|
||||
data-open={!collapsed}
|
||||
data-self-dragged-over={isSelfDraggedOver}
|
||||
ref={dropTargetRef}
|
||||
|
||||
Reference in New Issue
Block a user