feat(core): mobile renderer for explorer (#7942)

This commit is contained in:
CatsJuice
2024-08-29 06:09:45 +00:00
parent b96ad57568
commit f37051dc87
8 changed files with 210 additions and 82 deletions

View File

@@ -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,
},
]);

View File

@@ -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

View File

@@ -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';

View File

@@ -13,3 +13,8 @@ export const header = style({
}, },
}, },
}); });
// mobile
export const mobileContent = style({
paddingTop: 8,
});

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,
},
]);

View File

@@ -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,48 +308,43 @@ 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={styles.collapsedIconContainer} className={
mobile
? styles.mobileCollapsedIconContainer
: styles.collapsedIconContainer
}
> >
<ArrowDownSmallIcon <ArrowDownSmallIcon
className={styles.collapsedIcon} className={styles.collapsedIcon}
data-collapsed={collapsed !== false} data-collapsed={collapsed !== false}
/> />
</div> </div>
{emoji ? (
<div className={styles.emojiIcon}>{emoji}</div> <div className={clsx(mobile ? styles.mobileItemMain : styles.itemMain)}>
) : ( <div
Icon && ( className={mobile ? styles.mobileIconContainer : styles.iconContainer}
>
{emoji ??
(Icon && (
<Icon <Icon
className={styles.icon}
draggedOver={draggedOver && !isSelfDraggedOver} draggedOver={draggedOver && !isSelfDraggedOver}
treeInstruction={treeInstruction} treeInstruction={treeInstruction}
collapsed={collapsed} collapsed={collapsed}
/> />
) ))}
)}
</div> </div>
{renameable && renaming && (
<RenameModal
open
width={sidebarWidth - 32}
onOpenChange={setRenaming}
onRename={handleRename}
currentName={rawName ?? ''}
>
<div className={styles.itemRenameAnchor} />
</RenameModal>
)}
<div className={styles.itemContent}>{name}</div> <div className={mobile ? styles.mobileItemContent : styles.itemContent}>
{name}
</div>
{postfix} {postfix}
<div <div
@@ -378,6 +375,19 @@ export const ExplorerTreeNode = ({
)} )}
</div> </div>
</div> </div>
{renameable && renaming && (
<RenameModal
open
width={sidebarWidth - 32}
onOpenChange={setRenaming}
onRename={handleRename}
currentName={rawName ?? ''}
>
<div className={styles.itemRenameAnchor} />
</RenameModal>
)}
</div>
); );
return ( return (
@@ -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}