mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 21:25:45 +08: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;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
|
mobile?: boolean;
|
||||||
setCollapsed?: (collapsed: boolean) => void;
|
setCollapsed?: (collapsed: boolean) => void;
|
||||||
} & {
|
} & {
|
||||||
[key: `data-${string}`]: unknown;
|
[key: `data-${string}`]: unknown;
|
||||||
@@ -22,6 +23,7 @@ export const CategoryDivider = forwardRef(
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
collapsed,
|
collapsed,
|
||||||
|
mobile,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: CategoryDividerProps,
|
}: CategoryDividerProps,
|
||||||
@@ -31,14 +33,15 @@ export const CategoryDivider = forwardRef(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx([styles.root, className])}
|
className={clsx(mobile ? styles.mobileRoot : styles.root, className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => setCollapsed?.(!collapsed)}
|
onClick={() => setCollapsed?.(!collapsed)}
|
||||||
|
data-mobile={mobile}
|
||||||
data-collapsed={collapsed}
|
data-collapsed={collapsed}
|
||||||
data-collapsible={collapsible}
|
data-collapsible={collapsible}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.label}>
|
<div className={mobile ? styles.mobileLabel : styles.label}>
|
||||||
{label}
|
{label}
|
||||||
{collapsible ? (
|
{collapsible ? (
|
||||||
<ToggleCollapseIcon
|
<ToggleCollapseIcon
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ExplorerSection } from './entities/explore-section';
|
|||||||
import { ExplorerService } from './services/explorer';
|
import { ExplorerService } from './services/explorer';
|
||||||
export { ExplorerService } from './services/explorer';
|
export { ExplorerService } from './services/explorer';
|
||||||
export type { CollapsibleSectionName } from './types';
|
export type { CollapsibleSectionName } from './types';
|
||||||
|
export { ExplorerMobileContext } from './views/mobile.context';
|
||||||
export { ExplorerCollections } from './views/sections/collections';
|
export { ExplorerCollections } from './views/sections/collections';
|
||||||
export { ExplorerFavorites } from './views/sections/favorites';
|
export { ExplorerFavorites } from './views/sections/favorites';
|
||||||
export { ExplorerMigrationFavorites } from './views/sections/migration-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 ReactNode,
|
||||||
type RefObject,
|
type RefObject,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { ExplorerService } from '../../services/explorer';
|
import { ExplorerService } from '../../services/explorer';
|
||||||
import type { CollapsibleSectionName } from '../../types';
|
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 {
|
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||||
name: CollapsibleSectionName;
|
name: CollapsibleSectionName;
|
||||||
@@ -43,6 +50,7 @@ export const CollapsibleSection = ({
|
|||||||
|
|
||||||
contentClassName,
|
contentClassName,
|
||||||
}: CollapsibleSectionProps) => {
|
}: CollapsibleSectionProps) => {
|
||||||
|
const mobile = useContext(ExplorerMobileContext);
|
||||||
const section = useService(ExplorerService).sections[name];
|
const section = useService(ExplorerService).sections[name];
|
||||||
|
|
||||||
const collapsed = useLiveData(section.collapsed$);
|
const collapsed = useLiveData(section.collapsed$);
|
||||||
@@ -62,6 +70,7 @@ export const CollapsibleSection = ({
|
|||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
<CategoryDivider
|
<CategoryDivider
|
||||||
|
mobile={mobile}
|
||||||
data-testid={headerTestId}
|
data-testid={headerTestId}
|
||||||
label={title}
|
label={title}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
@@ -69,9 +78,11 @@ export const CollapsibleSection = ({
|
|||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className={clsx(header, headerClassName)}
|
className={clsx(header, headerClassName)}
|
||||||
>
|
>
|
||||||
{actions}
|
{mobile ? null : actions}
|
||||||
</CategoryDivider>
|
</CategoryDivider>
|
||||||
<Collapsible.Content className={clsx(content, contentClassName)}>
|
<Collapsible.Content
|
||||||
|
className={clsx(mobile ? mobileContent : content, contentClassName)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</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({
|
export const itemRenameAnchor = style({
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -55,31 +63,26 @@ export const itemContent = style({
|
|||||||
export const postfix = style({
|
export const postfix = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
right: '4px',
|
right: 0,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${itemRoot}:hover &`]: {
|
[`${itemRoot}:hover &`]: {
|
||||||
justifySelf: 'flex-end',
|
|
||||||
position: 'initial',
|
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'initial',
|
||||||
|
position: 'initial',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const icon = style({
|
export const iconContainer = style({
|
||||||
color: cssVarV2('icon/primary'),
|
|
||||||
fontSize: '20px',
|
|
||||||
});
|
|
||||||
export const emojiIcon = style({
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
color: cssVarV2('icon/primary'),
|
color: cssVarV2('icon/primary'),
|
||||||
fontSize: cssVar('--affine-font-sm'),
|
fontSize: 20,
|
||||||
});
|
});
|
||||||
export const collapsedIconContainer = style({
|
export const collapsedIconContainer = style({
|
||||||
width: '16px',
|
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({
|
export const collapsedIcon = style({
|
||||||
transition: 'transform 0.2s ease-in-out',
|
transition: 'transform 0.2s ease-in-out',
|
||||||
selectors: {
|
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,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { ExplorerMobileContext } from '../mobile.context';
|
||||||
import { ExplorerTreeContext } from './context';
|
import { ExplorerTreeContext } from './context';
|
||||||
import { DropEffect } from './drop-effect';
|
import { DropEffect } from './drop-effect';
|
||||||
import * as styles from './node.css';
|
import * as styles from './node.css';
|
||||||
@@ -108,6 +109,7 @@ export const ExplorerTreeNode = ({
|
|||||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||||
} & { [key in `data-${string}`]?: any }) => {
|
} & { [key in `data-${string}`]?: any }) => {
|
||||||
|
const mobile = useContext(ExplorerMobileContext);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const cid = useId();
|
const cid = useId();
|
||||||
const context = useContext(ExplorerTreeContext);
|
const context = useContext(ExplorerTreeContext);
|
||||||
@@ -306,35 +308,74 @@ export const ExplorerTreeNode = ({
|
|||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={styles.itemRoot}
|
className={mobile ? styles.mobileItemRoot : styles.itemRoot}
|
||||||
data-active={active}
|
data-active={active}
|
||||||
data-disabled={disabled}
|
data-disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className={styles.iconsContainer}>
|
<div
|
||||||
<div
|
data-disabled={disabled}
|
||||||
data-disabled={disabled}
|
onClick={handleCollapsedChange}
|
||||||
onClick={handleCollapsedChange}
|
data-testid="explorer-collapsed-button"
|
||||||
data-testid="explorer-collapsed-button"
|
className={
|
||||||
className={styles.collapsedIconContainer}
|
mobile
|
||||||
>
|
? styles.mobileCollapsedIconContainer
|
||||||
<ArrowDownSmallIcon
|
: styles.collapsedIconContainer
|
||||||
className={styles.collapsedIcon}
|
}
|
||||||
data-collapsed={collapsed !== false}
|
>
|
||||||
/>
|
<ArrowDownSmallIcon
|
||||||
</div>
|
className={styles.collapsedIcon}
|
||||||
{emoji ? (
|
data-collapsed={collapsed !== false}
|
||||||
<div className={styles.emojiIcon}>{emoji}</div>
|
/>
|
||||||
) : (
|
|
||||||
Icon && (
|
|
||||||
<Icon
|
|
||||||
className={styles.icon}
|
|
||||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
|
||||||
treeInstruction={treeInstruction}
|
|
||||||
collapsed={collapsed}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</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 && (
|
{renameable && renaming && (
|
||||||
<RenameModal
|
<RenameModal
|
||||||
open
|
open
|
||||||
@@ -346,37 +387,6 @@ export const ExplorerTreeNode = ({
|
|||||||
<div className={styles.itemRenameAnchor} />
|
<div className={styles.itemRenameAnchor} />
|
||||||
</RenameModal>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -391,7 +401,10 @@ export const ExplorerTreeNode = ({
|
|||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
|
className={clsx(
|
||||||
|
mobile ? styles.mobileContentContainer : styles.contentContainer,
|
||||||
|
styles.draggedOverEffect
|
||||||
|
)}
|
||||||
data-open={!collapsed}
|
data-open={!collapsed}
|
||||||
data-self-dragged-over={isSelfDraggedOver}
|
data-self-dragged-over={isSelfDraggedOver}
|
||||||
ref={dropTargetRef}
|
ref={dropTargetRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user