mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08:00
feat: add block card component (#2398)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const BlockCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
left?: ReactNode;
|
||||
title: string;
|
||||
desc?: string;
|
||||
right?: ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>
|
||||
>(({ left, title, desc, right, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={styles.blockCard} {...props}>
|
||||
{left && <div className={styles.blockCardAround}>{left}</div>}
|
||||
<div className={styles.blockCardContent}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.blockCardDesc}>{desc}</div>
|
||||
</div>
|
||||
{right && <div className={styles.blockCardAround}>{right}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
BlockCard.displayName = 'BlockCard';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const blockCard = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '8px 12px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
borderRadius: '4px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
},
|
||||
// TODO active styles
|
||||
},
|
||||
});
|
||||
|
||||
export const blockCardAround = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const blockCardContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const blockCardDesc = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { PermissionType } from '@affine/workspace/affine/api';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import {
|
||||
StyledCard,
|
||||
StyledSettingLink,
|
||||
StyleWorkspaceInfo,
|
||||
StyleWorkspaceTitle,
|
||||
} from './styles';
|
||||
|
||||
export type WorkspaceTypeProps = {
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
};
|
||||
|
||||
import {
|
||||
CloudWorkspaceIcon as DefaultCloudWorkspaceIcon,
|
||||
CollaborationIcon as DefaultJoinedWorkspaceIcon,
|
||||
LocalDataIcon as DefaultLocalDataIcon,
|
||||
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
|
||||
PublishIcon as DefaultPublishIcon,
|
||||
} from '@blocksuite/icons';
|
||||
|
||||
const JoinedWorkspaceIcon = () => {
|
||||
return <DefaultJoinedWorkspaceIcon style={{ color: '#FF646B' }} />;
|
||||
};
|
||||
const LocalWorkspaceIcon = () => {
|
||||
return <DefaultLocalWorkspaceIcon style={{ color: '#FDBD32' }} />;
|
||||
};
|
||||
|
||||
const CloudWorkspaceIcon = () => {
|
||||
return <DefaultCloudWorkspaceIcon style={{ color: '#60A5FA' }} />;
|
||||
};
|
||||
|
||||
const LocalDataIcon = () => {
|
||||
return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />;
|
||||
};
|
||||
const PublishIcon = () => {
|
||||
return <DefaultPublishIcon style={{ color: '#8699FF' }} />;
|
||||
};
|
||||
|
||||
const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
let isOwner = true;
|
||||
if (workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
isOwner = workspace.permission === PermissionType.Owner;
|
||||
} else if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
isOwner = true;
|
||||
}
|
||||
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return (
|
||||
<p title={t['Local Workspace']()}>
|
||||
<LocalWorkspaceIcon />
|
||||
<span>{t['Local Workspace']()}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return isOwner ? (
|
||||
<p title={t['Cloud Workspace']()}>
|
||||
<CloudWorkspaceIcon />
|
||||
<span>{t['Cloud Workspace']()}</span>
|
||||
</p>
|
||||
) : (
|
||||
<p title={t['Joined Workspace']()}>
|
||||
<JoinedWorkspaceIcon />
|
||||
<span>{t['Joined Workspace']()}</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export type WorkspaceCardProps = {
|
||||
currentWorkspaceId: string | null;
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
onClick: (workspace: AffineLegacyCloudWorkspace | LocalWorkspace) => void;
|
||||
onSettingClick: (
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const WorkspaceCard: FC<WorkspaceCardProps> = ({
|
||||
workspace,
|
||||
onClick,
|
||||
onSettingClick,
|
||||
currentWorkspaceId,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
return (
|
||||
<StyledCard
|
||||
data-testid="workspace-card"
|
||||
onClick={useCallback(() => {
|
||||
onClick(workspace);
|
||||
}, [onClick, workspace])}
|
||||
active={workspace.id === currentWorkspaceId}
|
||||
>
|
||||
<WorkspaceAvatar size={58} workspace={workspace} />
|
||||
|
||||
<StyleWorkspaceInfo>
|
||||
<StyleWorkspaceTitle>{name}</StyleWorkspaceTitle>
|
||||
<WorkspaceType workspace={workspace} />
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL && (
|
||||
<p title={t['Available Offline']()}>
|
||||
<LocalDataIcon />
|
||||
<span>{t['Available Offline']()}</span>
|
||||
</p>
|
||||
)}
|
||||
{workspace.flavour === WorkspaceFlavour.AFFINE && workspace.public && (
|
||||
<p title={t['Published to Web']()}>
|
||||
<PublishIcon />
|
||||
<span>{t['Published to Web']()}</span>
|
||||
</p>
|
||||
)}
|
||||
</StyleWorkspaceInfo>
|
||||
<StyledSettingLink
|
||||
className="setting-entry"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSettingClick(workspace);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</StyledSettingLink>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { displayFlex, styled, textEllipsis } from '../../../styles';
|
||||
import { IconButton } from '../../../ui/button/icon-button';
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
maxWidth: '200px',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCard = styled('div')<{
|
||||
active?: boolean;
|
||||
}>(({ active }) => {
|
||||
const borderColor = active ? 'var(--affine-primary-color)' : 'transparent';
|
||||
return {
|
||||
width: '310px',
|
||||
height: '124px',
|
||||
cursor: 'pointer',
|
||||
padding: '16px',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${borderColor}`,
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
marginBottom: '24px',
|
||||
transition: 'background .2s',
|
||||
background: 'var(--affine-white-80)',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
'.add-icon': {
|
||||
borderColor: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
'.setting-entry': {
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '72px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '0 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingLink = styled(IconButton)(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '6px',
|
||||
bottom: '6px',
|
||||
opacity: 0,
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
pointerEvents: 'none',
|
||||
transition: 'all .15s',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user