refactor(component): refactor the implementation of Button and IconButton (#7716)

## Button
- Remove props withoutHoverStyle
   refactor hover impl with independent layer, so that hover-color won't affect the background even if is overridden outside
- Update `type` (renamed to `variant`):
  - remove `processing` and `warning`
  - rename `default` with `secondary`
- Remove `shape` props
- Remove `icon` and `iconPosition`, replaced with `prefix: ReactNode` and `suffix: ReactNode`
- Integrate tooltip for more convenient usage
- New Storybook document
- Focus style

## IconButton
- A Wrapper base on `<Button />`
- Override Button size and variant
  - size: `'12' | '14' | '16' | '20' | '24' | number`
     These presets size are referenced from the design system.
  - variant:  `'plain' | 'solid' | 'danger' | 'custom'`
- Inset icon via Button 's prefix

## Fix
- fix some button related issues
- close AF-1159, AF-1160, AF-1161, AF-1162, AF-1163, AF-1158, AF-1157

## Storybook

![CleanShot 2024-08-03 at 14.57.20@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/f5a76110-35d0-4082-a940-efc12bed87b0.png)
This commit is contained in:
CatsJuice
2024-08-05 02:57:23 +00:00
parent 10deed94e3
commit 3d855647c7
159 changed files with 1384 additions and 1539 deletions
@@ -1,6 +1,7 @@
import { Button, IconButton } from '@affine/component/ui/button';
import { useI18n } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import { useCallback } from 'react';
import * as styles from './index.css';
@@ -37,11 +38,12 @@ export const LocalDemoTips = ({
</div>
<div className={styles.tipsRightItem}>
<div>
<Button onClick={handleClick}>{buttonLabel}</Button>
</div>
<Button style={{ background: cssVar('white') }} onClick={handleClick}>
{buttonLabel}
</Button>
<IconButton
onClick={onClose}
size="20"
data-testid="local-demo-tips-close-button"
>
<CloseIcon />
@@ -41,7 +41,7 @@ export const MobileNavbar = () => {
onOpenChange: setOpenMenu,
}}
>
<IconButton type="plain" className={styles.iconButton}>
<IconButton variant="plain" size="24" className={styles.iconButton}>
{openMenu ? <CloseIcon /> : <PropertyIcon />}
</IconButton>
</Menu>
@@ -1,5 +1,6 @@
import { useI18n } from '@affine/i18n';
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import type { FC } from 'react';
import type { ButtonProps } from '../../ui/button';
@@ -9,15 +10,15 @@ export const BackButton: FC<ButtonProps> = props => {
const t = useI18n();
return (
<Button
type="plain"
variant="plain"
style={{
marginTop: 12,
marginLeft: -5,
paddingLeft: 0,
paddingRight: 5,
color: 'var(--affine-text-secondary-color)',
color: cssVar('textSecondaryColor'),
}}
icon={<ArrowLeftSmallIcon />}
prefix={<ArrowLeftSmallIcon />}
{...props}
>
{t['com.affine.backButton']()}
@@ -58,7 +58,7 @@ export const ChangeEmailPage = ({
disabled={hasSetUp}
/>
<Button
type="primary"
variant="primary"
size="large"
onClick={onContinue}
loading={loading}
@@ -59,7 +59,7 @@ export const ChangePasswordPage: FC<{
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
@@ -14,7 +14,7 @@ export const ConfirmChangeEmail: FC<{
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
@@ -14,7 +14,7 @@ export const ConfirmChangeEmail: FC<{
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
@@ -219,7 +219,7 @@ export const OnboardingPage = ({
</Button>
<Button
className={styles.button}
type="primary"
variant="primary"
size="extraLarge"
itemType="submit"
onClick={() => {
@@ -248,8 +248,7 @@ export const OnboardingPage = ({
setQuestionIdx(questionIdx + 1);
}
}}
iconPosition="end"
icon={<ArrowRightSmallIcon />}
suffix={<ArrowRightSmallIcon />}
>
{questionIdx === 0 ? 'start' : 'Next'}
</Button>
@@ -271,7 +270,7 @@ export const OnboardingPage = ({
</p>
<Button
className={clsx(styles.button, styles.openAFFiNEButton)}
type="primary"
variant="primary"
size="extraLarge"
onClick={() => {
if (callbackUrl) {
@@ -280,8 +279,7 @@ export const OnboardingPage = ({
onOpenAffine();
}
}}
iconPosition="end"
icon={<ArrowRightSmallIcon />}
suffix={<ArrowRightSmallIcon />}
>
Get Started
</Button>
@@ -59,7 +59,7 @@ export const SetPasswordPage: FC<{
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
@@ -33,7 +33,7 @@ export const SetPassword: FC<{
/>
</Wrapper>
<Button
type="primary"
variant="primary"
size="large"
disabled={!passwordPass}
style={{ marginRight: 20 }}
@@ -44,7 +44,7 @@ export const SetPassword: FC<{
{t['com.affine.auth.set.password.save']()}
</Button>
{showLater ? (
<Button type="plain" size="large" onClick={onLater}>
<Button variant="plain" size="large" onClick={onLater}>
{t['com.affine.auth.later']()}
</Button>
) : null}
@@ -13,7 +13,7 @@ export const SignInSuccessPage: FC<{
title={t['com.affine.auth.signed.success.title']()}
subtitle={t['com.affine.auth.signed.success.subtitle']()}
>
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
@@ -63,7 +63,7 @@ export const SignUpPage: FC<{
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{openButtonText ?? t['com.affine.auth.open.affine']()}
</Button>
) : (
@@ -3,7 +3,6 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons/rc';
import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx';
import { type MouseEvent, useCallback } from 'react';
import { Button } from '../../../ui/button';
@@ -89,8 +88,7 @@ export const WorkspaceCard = ({
<Button
loading={!!openingId && openingId === meta.id}
disabled={!!openingId}
type="default"
className={clsx(styles.enableCloudButton, styles.showOnCardHover)}
className={styles.showOnCardHover}
onClick={onEnableCloud}
>
{enableCloudText}
@@ -79,21 +79,16 @@ export const settingButton = style({
boxShadow: cssVar('shadow1'),
background: cssVar('white80'),
},
// [`.${card}:hover &:hover`]: {
// background: cssVar('hoverColor'),
// },
},
});
export const enableCloudButton = style({
background: 'transparent',
});
export const showOnCardHover = style({
display: 'none',
visibility: 'hidden',
opacity: 0,
selectors: {
[`.${card}:hover &`]: {
display: 'block',
visibility: 'visible',
opacity: 1,
},
},
});
@@ -13,7 +13,7 @@ export const PublicLinkDisableModal = (props: ConfirmModalProps) => {
cancelText={t['com.affine.publicLinkDisableModal.button.cancel']()}
confirmText={t['com.affine.publicLinkDisableModal.button.disable']()}
confirmButtonOptions={{
type: 'error',
variant: 'error',
['data-testid' as string]: 'confirm-enable-affine-cloud-button',
}}
{...props}
@@ -16,6 +16,9 @@ import {
importPageContainerStyle,
} from './index.css';
/**
* @deprecated Not used
*/
export const ImportPage = ({
importMarkdown,
importHtml,
@@ -37,8 +40,9 @@ export const ImportPage = ({
onClick={() => {
onClose();
}}
icon={<CloseIcon />}
/>
>
<CloseIcon />
</IconButton>
<div className={importPageBodyStyle}>
<div className="title">Import</div>
<span>
@@ -37,7 +37,7 @@ export const AcceptInvitePage = ({
</FlexWrapper>
}
>
<Button type="primary" size="large" onClick={onOpenWorkspace}>
<Button variant="primary" size="large" onClick={onOpenWorkspace}>
{t['Visit Workspace']()}
</Button>
</AuthPageContainer>
@@ -60,7 +60,7 @@ export const InviteModal = ({
confirmText={t['Invite']()}
confirmButtonOptions={{
loading: isMutating,
type: 'primary',
variant: 'primary',
['data-testid' as string]: 'confirm-enable-affine-cloud-button',
}}
onConfirm={handleConfirm}
@@ -44,7 +44,7 @@ export const MemberLimitModal = ({
: 'com.affine.payment.member-limit.pro.confirm'
]()}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
onConfirm={handleConfirm}
></ConfirmModal>
@@ -3,7 +3,6 @@ import { SignOutIcon } from '@blocksuite/icons/rc';
import { Avatar } from '../../ui/avatar';
import { Button, IconButton } from '../../ui/button';
import { Tooltip } from '../../ui/tooltip';
import { AffineOtherPageLayout } from '../affine-other-page-layout';
import type { User } from '../auth-components';
import { NotFoundPattern } from './not-found-pattern';
@@ -38,7 +37,7 @@ export const NoPermissionOrNotFound = ({
<p className={wrapper}>{t['404.hint']()}</p>
<div className={wrapper}>
<Button
type="primary"
variant="primary"
size="extraLarge"
onClick={onBack}
className={largeButtonEffect}
@@ -49,11 +48,13 @@ export const NoPermissionOrNotFound = ({
<div className={wrapper}>
<Avatar url={user.avatar ?? user.image} name={user.label} />
<span style={{ margin: '0 12px' }}>{user.email}</span>
<Tooltip content={t['404.signOut']()}>
<IconButton onClick={onSignOut}>
<SignOutIcon />
</IconButton>
</Tooltip>
<IconButton
onClick={onSignOut}
size="20"
tooltip={t['404.signOut']()}
>
<SignOutIcon />
</IconButton>
</div>
</>
) : (
@@ -80,7 +81,7 @@ export const NotFoundPage = ({
<p className={wrapper}>{t['404.hint']()}</p>
<div className={wrapper}>
<Button
type="primary"
variant="primary"
size="extraLarge"
onClick={onBack}
className={largeButtonEffect}
@@ -93,11 +94,13 @@ export const NotFoundPage = ({
<div className={wrapper}>
<Avatar url={user.avatar ?? user.image} name={user.label} />
<span style={{ margin: '0 12px' }}>{user.email}</span>
<Tooltip content={t['404.signOut']()}>
<IconButton onClick={onSignOut}>
<SignOutIcon />
</IconButton>
</Tooltip>
<IconButton
onClick={onSignOut}
size="20"
tooltip={t['404.signOut']()}
>
<SignOutIcon />
</IconButton>
</div>
) : null}
</div>
@@ -199,21 +199,20 @@ export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(
</Tooltip>
{onRemove ? (
<Tooltip
portalOptions={{ container: removeButtonDom }}
{...removeTooltipOptions}
<IconButton
tooltipOptions={{
portalOptions: { container: removeButtonDom },
...removeTooltipOptions,
}}
variant="solid"
size="12"
className={clsx(style.removeButton, removeButtonClassName)}
onClick={onRemove}
ref={setRemoveButtonDom}
{...removeButtonProps}
>
<IconButton
size="extraSmall"
type="default"
className={clsx(style.removeButton, removeButtonClassName)}
onClick={onRemove}
ref={setRemoveButtonDom}
{...removeButtonProps}
>
<CloseIcon />
</IconButton>
</Tooltip>
<CloseIcon />
</IconButton>
) : null}
</AvatarRoot>
);
@@ -1,5 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css';
import { createVar, keyframes, style } from '@vanilla-extract/css';
export const sizeVar = createVar('sizeVar');
export const blurVar = createVar('blurVar');
const bottomAnimation = keyframes({
@@ -172,7 +172,7 @@ export const hoverWrapper = style({
alignItems: 'center',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
zIndex: '1',
color: cssVar('white'),
color: cssVar('pureWhite'),
opacity: 0,
transition: 'opacity .15s',
cursor: 'pointer',
@@ -189,14 +189,8 @@ export const removeButton = style({
visibility: 'hidden',
zIndex: '1',
selectors: {
'&:hover': {
background: '#f6f6f6',
[`${avatarRoot}:hover &`]: {
visibility: 'visible',
},
},
});
globalStyle(`${avatarRoot}:hover ${removeButton}`, {
visibility: 'visible',
});
globalStyle(`${avatarRoot} ${removeButton}:hover`, {
background: '#f6f6f6',
});
@@ -1,371 +1,259 @@
import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
import { cssVarV2 } from '@toeverything/theme/v2';
import { createVar, globalStyle, style } from '@vanilla-extract/css';
// Using variables can override externally, without considering the priority of selectors.
// size vars
export const hVar = createVar('height');
export const wVar = createVar('width');
export const iconSizeVar = createVar('iconSize');
const gapVar = createVar('gap');
const paddingVar = createVar('padding');
const fontSizeVar = createVar('fontSize');
const fontWeightVar = createVar('fontWeight');
const lineHeightVar = createVar('lineHeight');
const shadowVar = createVar('shadow');
// style vars
const bgVar = createVar('bg');
const textVar = createVar('fg');
const iconColorVar = createVar('icon');
const borderColorVar = createVar('border');
const borderWidthVar = createVar('borderWidth');
export const button = style({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
userSelect: 'none',
touchAction: 'manipulation',
vars: {
// default vars
[gapVar]: '4px',
[wVar]: 'unset',
[hVar]: 'unset',
[borderWidthVar]: '1px',
},
flexShrink: 0,
outline: '0',
border: '1px solid',
padding: '0 8px',
borderRadius: '8px',
fontSize: cssVar('fontXs'),
fontWeight: 500,
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
outline: 0,
borderRadius: 8,
transition: 'all .3s',
['WebkitAppRegion' as string]: 'no-drag',
cursor: 'pointer',
// changeable
height: '28px',
background: cssVar('white'),
borderColor: cssVar('borderColor'),
color: cssVar('textPrimaryColor'),
['WebkitAppRegion' as string]: 'no-drag',
// hover layer
':before': {
content: '""',
position: 'absolute',
width: '100%',
height: '100%',
transition: 'inherit',
borderRadius: 'inherit',
opacity: 0,
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
borderColor: 'transparent',
pointerEvents: 'none',
borderWidth: 'inherit',
borderStyle: 'inherit',
},
// style
backgroundColor: bgVar,
color: textVar,
boxShadow: shadowVar,
borderWidth: borderWidthVar,
borderStyle: 'solid',
borderColor: borderColorVar,
// size
width: wVar,
height: hVar,
gap: gapVar,
padding: paddingVar,
fontSize: fontSizeVar,
fontWeight: fontWeightVar,
lineHeight: lineHeightVar,
selectors: {
'&.text-bold': {
fontWeight: 600,
},
'&:not(.without-hover):hover': {
background: cssVar('hoverColor'),
},
'&.disabled': {
opacity: '.4',
cursor: 'default',
color: cssVar('textDisableColor'),
pointerEvents: 'none',
},
'&.loading': {
cursor: 'default',
color: cssVar('textDisableColor'),
pointerEvents: 'none',
},
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
{
background: 'inherit',
'&:hover:before': { opacity: 1 },
'&[data-block]': { display: 'flex' },
// size
'&[data-size="default"]': {
vars: {
[hVar]: '28px', // line-height + paddingY * 2 (to ignore border width)
[paddingVar]: '0px 8px',
[iconSizeVar]: '16px',
[paddingVar]: '4px 12px',
[fontSizeVar]: cssVar('fontXs'),
[fontWeightVar]: '500',
[lineHeightVar]: '20px',
},
'&.block': {
display: 'flex',
},
'&[data-size="large"]': {
vars: {
[hVar]: '32px',
[paddingVar]: '0px 8px',
[iconSizeVar]: '20px',
[paddingVar]: '4px 12px',
[fontSizeVar]: '15px',
[fontWeightVar]: '500',
[lineHeightVar]: '24px',
},
},
'&[data-size="extraLarge"]': {
vars: {
[hVar]: '40px',
[paddingVar]: '0px 8px',
[iconSizeVar]: '24px',
[paddingVar]: '8px 18px',
[fontSizeVar]: '15',
[fontWeightVar]: '600',
[lineHeightVar]: '24px',
},
},
// type
'&[data-variant="primary"]': {
vars: {
[bgVar]: cssVarV2('button/primary'),
[textVar]: cssVarV2('button/pureWhiteText'),
[iconColorVar]: cssVarV2('button/pureWhiteText'),
[borderColorVar]: cssVarV2('button/innerBlackBorder'),
},
},
'&[data-variant="secondary"]': {
vars: {
[bgVar]: cssVarV2('button/secondary'),
[textVar]: cssVarV2('text/primary'),
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: cssVarV2('layer/border'),
},
},
'&[data-variant="plain"]': {
vars: {
[bgVar]: 'transparent',
[textVar]: cssVarV2('text/primary'),
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[borderWidthVar]: '0px',
},
},
'&[data-variant="error"]': {
vars: {
[bgVar]: cssVarV2('button/error'),
[textVar]: cssVarV2('button/pureWhiteText'),
[iconColorVar]: cssVarV2('button/pureWhiteText'),
[borderColorVar]: cssVarV2('button/innerBlackBorder'),
},
},
'&[data-variant="success"]': {
vars: {
[bgVar]: cssVarV2('button/success'),
[textVar]: cssVarV2('button/pureWhiteText'),
[iconColorVar]: cssVarV2('button/pureWhiteText'),
[borderColorVar]: cssVarV2('button/innerBlackBorder'),
},
},
// disabled
'&[data-disabled]': {
cursor: 'not-allowed',
opacity: 0.5,
},
// default keyboard focus style
'&:focus-visible::after': {
content: '""',
width: '100%',
},
'&.circle': {
borderRadius: '50%',
},
'&.round': {
borderRadius: '14px',
},
// size
'&.large': {
height: '32px',
fontSize: cssVar('fontBase'),
fontWeight: 600,
},
'&.round.large': {
borderRadius: '16px',
},
'&.extraLarge': {
height: '40px',
fontSize: cssVar('fontBase'),
fontWeight: 700,
},
'&.extraLarge.primary': {
boxShadow: `${cssVar('largeButtonEffect')} !important`,
},
'&.round.extraLarge': {
borderRadius: '20px',
},
// type
'&.plain': {
color: cssVar('textPrimaryColor'),
borderColor: 'transparent',
background: 'transparent',
},
'&.primary': {
color: cssVar('pureWhite'),
background: cssVar('primaryColor'),
borderColor: cssVar('black10'),
},
'&.primary:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'primaryColor'
)}`,
},
'&.primary.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.primary.disabled:not(.without-hover):hover': {
background: cssVar('primaryColor'),
},
'&.error': {
color: cssVar('pureWhite'),
background: cssVar('errorColor'),
borderColor: cssVar('black10'),
},
'&.error:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'errorColor'
)}`,
},
'&.error.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.error.disabled:not(.without-hover):hover': {
background: cssVar('errorColor'),
},
'&.warning': {
color: cssVar('pureWhite'),
background: cssVar('warningColor'),
borderColor: cssVar('black10'),
},
'&.warning:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'warningColor'
)}`,
},
'&.warning.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.warning.disabled:not(.without-hover):hover': {
background: cssVar('warningColor'),
},
'&.success': {
color: cssVar('pureWhite'),
background: cssVar('successColor'),
borderColor: cssVar('black10'),
},
'&.success:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'successColor'
)}`,
},
'&.success.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.success.disabled:not(.without-hover):hover': {
background: cssVar('successColor'),
},
'&.processing': {
color: cssVar('pureWhite'),
background: cssVar('processingColor'),
borderColor: cssVar('black10'),
},
'&.processing:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'processingColor'
)}`,
},
'&.processing.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.processing.disabled:not(.without-hover):hover': {
background: cssVar('processingColor'),
},
'&.danger:hover': {
color: cssVar('errorColor'),
background: cssVar('backgroundErrorColor'),
height: '100%',
position: 'absolute',
top: 0,
left: 0,
borderRadius: 'inherit',
boxShadow: `0 0 0 1px ${cssVarV2('layer/insideBorder/primary')}`,
},
},
});
globalStyle(`${button} > span`, {
// flex: 1,
lineHeight: 1,
padding: '0 4px',
export const content = style({
// in case that width is specified by parent and text is too long
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
});
export const buttonIcon = style({
export const icon = style({
flexShrink: 0,
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
color: cssVar('iconColor'),
fontSize: '16px',
width: '16px',
height: '16px',
selectors: {
'&.start': {
marginRight: '4px',
},
'&.end': {
marginLeft: '4px',
},
'&.large': {
fontSize: '20px',
width: '20px',
height: '20px',
},
'&.extraLarge': {
fontSize: '20px',
width: '20px',
height: '20px',
},
'&.color-white': {
color: cssVar('pureWhite'),
},
},
// There are two kinds of icon size:
// 1. control by props: width and height
width: iconSizeVar,
height: iconSizeVar,
// 2. width/height is set to `1em`
fontSize: iconSizeVar,
color: iconColorVar,
});
globalStyle(`${icon} > svg`, {
width: '100%',
height: '100%',
display: 'block',
});
export const iconButton = style({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
userSelect: 'none',
touchAction: 'manipulation',
outline: '0',
border: '1px solid',
borderRadius: '4px',
transition: 'all .3s',
['WebkitAppRegion' as string]: 'no-drag',
cursor: 'pointer',
background: cssVar('white'),
// changeable
width: '24px',
height: '24px',
fontSize: '20px',
color: cssVar('textPrimaryColor'),
borderColor: cssVar('borderColor'),
vars: {
[paddingVar]: '2px',
// TODO(@CatsJuice): Replace with theme variables when ready
'--shadow':
'0px 0px 1px 0px rgba(0, 0, 0, 0.12), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)',
},
borderRadius: 4,
selectors: {
'&.without-padding': {
margin: '-2px',
},
'&.active': {
color: cssVar('primaryColor'),
},
'&:not(.without-hover):hover': {
background: cssVar('hoverColor'),
},
'&.disabled': {
opacity: '.4',
cursor: 'default',
color: cssVar('textDisableColor'),
pointerEvents: 'none',
},
'&.loading': {
cursor: 'default',
color: cssVar('textDisableColor'),
pointerEvents: 'none',
},
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
{
background: 'inherit',
'[data-theme="dark"] &': {
vars: {
'--shadow':
'0px 0px 1px 0px rgba(0, 0, 0, 0.66), 0px 1px 5px 0px rgba(0, 0, 0, 0.72)',
},
// size
'&.large': {
width: '32px',
height: '32px',
fontSize: '24px',
},
'&.large.without-padding': {
margin: '-4px',
'&[data-icon-variant="plain"]': {
vars: {
[bgVar]: 'transparent',
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[borderWidthVar]: '0px',
},
},
'&.small': {
width: '20px',
height: '20px',
fontSize: '16px',
'&[data-icon-variant="danger"]': {
vars: {
[bgVar]: 'transparent',
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[borderWidthVar]: '0px',
},
},
'&.extra-small': {
width: '16px',
height: '16px',
fontSize: '12px',
'&[data-icon-variant="danger"]:hover': {
vars: {
[bgVar]: cssVar('backgroundErrorColor'),
[iconColorVar]: cssVar('errorColor'),
},
},
// type
'&.plain': {
color: cssVar('iconColor'),
borderColor: 'transparent',
background: 'transparent',
// disable hover layer for danger type
'&[data-icon-variant="danger"]:hover:before': {
opacity: 0,
},
'&.plain.active': {
color: cssVar('primaryColor'),
'&[data-icon-variant="solid"]': {
vars: {
[bgVar]: cssVarV2('button/iconButtonSolid'),
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[shadowVar]: 'var(--shadow)',
},
},
'&.primary': {
color: cssVar('white'),
background: cssVar('primaryColor'),
borderColor: cssVar('black10'),
},
'&.primary:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'primaryColor'
)}`,
},
'&.primary.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.primary.disabled:not(.without-hover):hover': {
background: cssVar('primaryColor'),
},
'&.error': {
color: cssVar('white'),
background: cssVar('errorColor'),
borderColor: cssVar('black10'),
},
'&.error:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'errorColor'
)}`,
},
'&.error.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.error.disabled:not(.without-hover):hover': {
background: cssVar('errorColor'),
},
'&.warning': {
color: cssVar('white'),
background: cssVar('warningColor'),
borderColor: cssVar('black10'),
},
'&.warning:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'warningColor'
)}`,
},
'&.warning.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.warning.disabled:not(.without-hover):hover': {
background: cssVar('warningColor'),
},
'&.success': {
color: cssVar('white'),
background: cssVar('successColor'),
borderColor: cssVar('black10'),
},
'&.success:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'successColor'
)}`,
},
'&.success.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.success.disabled:not(.without-hover):hover': {
background: cssVar('successColor'),
},
'&.processing': {
color: cssVar('white'),
background: cssVar('processingColor'),
borderColor: cssVar('black10'),
},
'&.processing:not(.without-hover):hover': {
background: `linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), ${cssVar(
'processingColor'
)}`,
},
'&.processing.disabled': {
opacity: '.4',
cursor: 'default',
},
'&.processing.disabled:not(.without-hover):hover': {
background: cssVar('processingColor'),
},
'&.danger:hover': {
color: cssVar('errorColor'),
background: cssVar('backgroundErrorColor'),
'&[data-icon-size="24"]': {
vars: { [paddingVar]: '4px' },
},
},
});
@@ -0,0 +1,61 @@
import { globalStyle, style } from '@vanilla-extract/css';
// table
export const table = style({
vars: { '--border-color': '#974FFF' },
});
globalStyle(`${table} thead td, ${table} tbody tr td:nth-child(1)`, {
backgroundColor: '#974FFF10',
padding: '16px',
fontWeight: 600,
fontSize: 12,
color: 'var(--border-color)',
});
globalStyle(`${table} td`, {
textAlign: 'center',
border: '0.5px dashed var(--border-color)',
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
padding: '16px 8px',
});
globalStyle(`${table} thead td`, {
borderTopColor: 'var(--border-color)',
});
globalStyle(`${table} tbody tr:last-child td`, {
borderBottomColor: 'var(--border-color)',
});
export const settings = style({
display: 'flex',
flexWrap: 'wrap',
gap: '8px 100px',
marginBottom: 40,
});
globalStyle(`${settings} > section`, {
display: 'flex',
alignItems: 'center',
});
globalStyle(`${settings} > section > span`, {
display: 'inline-block',
width: 200,
});
export const overrideBackground = style({
background: 'cyan',
});
export const overrideTextColor = style({
color: 'red',
});
export const overrideBorder = style({
borderColor: 'green',
});
export const overrideFontSize = style({
fontSize: 24,
});
export const overrideIconSize = style({
width: 60,
height: 60,
});
export const overrideIconColor = style({
color: 'forestgreen',
});
@@ -1,47 +1,183 @@
import { InformationIcon } from '@blocksuite/icons/rc';
import type { Meta, StoryFn } from '@storybook/react';
import {
AfFiNeIcon,
ArrowRightBigIcon,
FolderIcon,
} from '@blocksuite/icons/rc';
import type { Meta } from '@storybook/react';
import clsx from 'clsx';
import { useCallback, useEffect, useState } from 'react';
import { Switch } from '../switch';
import type { ButtonProps } from './button';
import { Button } from './button';
import * as styles from './button.stories.css';
export default {
title: 'UI/Button',
component: Button,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<ButtonProps>;
const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
// const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
export const Default: StoryFn<ButtonProps> = Template.bind(undefined);
Default.args = {
type: 'default',
children: 'This is a default button',
icon: <InformationIcon />,
const types: ButtonProps['variant'][] = [
'primary',
'secondary',
'plain',
'error',
'success',
];
const sizes: ButtonProps['size'][] = ['default', 'large', 'extraLarge'];
const Groups = ({
children,
...props
}: Omit<ButtonProps, 'variant' | 'size'>) => {
return (
<table className={styles.table}>
<thead>
<tr>
<td>Type/Size</td>
{sizes.map(size => (
<td key={size}>{size}</td>
))}
</tr>
</thead>
<tbody>
{types.map(type => (
<tr key={type}>
<td>{type}</td>
{sizes.map(size => (
<td key={size}>
<Button variant={type} size={size} {...props}>
{children ?? `${size} - ${type}`}
</Button>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export const Primary: StoryFn<ButtonProps> = Template.bind(undefined);
Primary.args = {
type: 'primary',
children: 'Content',
icon: <InformationIcon />,
export const Default = () => <Groups />;
export const WithIcon = () => {
return <Groups prefix={<FolderIcon />} suffix={<span>🚀</span>} />;
};
export const Disabled: StoryFn<ButtonProps> = Template.bind(undefined);
Disabled.args = {
disabled: true,
children: 'This is a disabled button',
export const Loading = () => {
const [loading, setLoading] = useState(false);
const toggleLoading = useCallback(() => setLoading(v => !v), []);
useEffect(() => {
setInterval(toggleLoading, 1000);
}, [toggleLoading]);
return <Groups loading={loading} prefix={<FolderIcon />} />;
};
export const LargeSizeButton: StoryFn<ButtonProps> = Template.bind(undefined);
LargeSizeButton.args = {
size: 'large',
children: 'This is a large button',
export const OverrideViaClassName = () => {
const [overrideBg, setOverrideBg] = useState(false);
const [overrideTextColor, setOverrideTextColor] = useState(false);
const [overrideBorder, setOverrideBorder] = useState(false);
const [overrideFontSize, setOverrideFontSize] = useState(false);
const [overridePrefixSize, setOverridePrefixSize] = useState(false);
const [overrideSuffixSize, setOverrideSuffixSize] = useState(false);
const [overridePrefixColor, setOverridePrefixColor] = useState(false);
const [overrideSuffixColor, setOverrideSuffixColor] = useState(false);
return (
<div>
<div className={styles.settings}>
<section>
<span>Override background color</span>
<Switch checked={overrideBg} onChange={setOverrideBg} />
</section>
<section>
<span>Override text color</span>
<Switch checked={overrideTextColor} onChange={setOverrideTextColor} />
</section>
<section>
<span>Override border color</span>
<Switch checked={overrideBorder} onChange={setOverrideBorder} />
</section>
<section>
<span>Override font size</span>
<Switch checked={overrideFontSize} onChange={setOverrideFontSize} />
</section>
<section>
<span>Override prefix size</span>
<Switch
checked={overridePrefixSize}
onChange={setOverridePrefixSize}
/>
</section>
<section>
<span>Override suffix size</span>
<Switch
checked={overrideSuffixSize}
onChange={setOverrideSuffixSize}
/>
</section>
<section>
<span>Override prefix color</span>
<Switch
checked={overridePrefixColor}
onChange={setOverridePrefixColor}
/>
</section>
<section>
<span>Override suffix color</span>
<Switch
checked={overrideSuffixColor}
onChange={setOverrideSuffixColor}
/>
</section>
</div>
<Groups
prefix={<FolderIcon />}
suffix={<ArrowRightBigIcon />}
className={clsx({
[styles.overrideBackground]: overrideBg,
[styles.overrideTextColor]: overrideTextColor,
[styles.overrideBorder]: overrideBorder,
[styles.overrideFontSize]: overrideFontSize,
})}
prefixClassName={clsx({
[styles.overrideIconSize]: overridePrefixSize,
[styles.overrideIconColor]: overridePrefixColor,
})}
suffixClassName={clsx({
[styles.overrideIconSize]: overrideSuffixSize,
[styles.overrideIconColor]: overrideSuffixColor,
})}
/>
</div>
);
};
export const ExtraLargeSizeButton: StoryFn<ButtonProps> =
Template.bind(undefined);
ExtraLargeSizeButton.args = {
size: 'extraLarge',
children: 'This is a extra large button',
export const FixedWidth = () => {
const widths = [60, 100, 120, 160, 180];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{widths.map(width => (
<Button prefix={<AfFiNeIcon />} key={width} style={{ width }}>
This is a width fixed button
</Button>
))}
</div>
);
};
export const Disabled = () => {
return <Groups disabled />;
};
@@ -1,172 +1,168 @@
import clsx from 'clsx';
import type {
FC,
CSSProperties,
HTMLAttributes,
PropsWithChildren,
MouseEvent,
ReactElement,
} from 'react';
import { forwardRef, useMemo } from 'react';
import { cloneElement, forwardRef, useCallback } from 'react';
import { Loading } from '../loading';
import { button, buttonIcon } from './button.css';
import { Tooltip, type TooltipProps } from '../tooltip';
import * as styles from './button.css';
export type ButtonType =
| 'default'
| 'primary'
| 'secondary'
| 'plain'
| 'error'
| 'warning'
| 'success'
| 'processing';
export type ButtonSize = 'default' | 'large' | 'extraLarge';
type BaseButtonProps = {
type?: ButtonType;
| 'custom';
export type ButtonSize = 'default' | 'large' | 'extraLarge' | 'custom';
export interface ButtonProps
extends Omit<HTMLAttributes<HTMLButtonElement>, 'type' | 'prefix'> {
/**
* Preset color scheme
* @default 'secondary'
*/
variant?: ButtonType;
disabled?: boolean;
icon?: ReactElement;
iconPosition?: 'start' | 'end';
shape?: 'default' | 'round' | 'circle';
/**
* By default, the button is `inline-flex`, set to `true` to make it `flex`
* @default false
*/
block?: boolean;
/**
* Preset size, will be overridden by `style` or `className`
* @default 'default'
*/
size?: ButtonSize;
/**
* Will show a loading spinner at `prefix` position
*/
loading?: boolean;
withoutHoverStyle?: boolean;
};
export type ButtonProps = PropsWithChildren<BaseButtonProps> &
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
componentProps?: {
startIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
endIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
};
};
/**
* By default, it is considered as an icon with preset size and color,
* can be overridden by `prefixClassName` and `prefixStyle`.
*
* If `loading` is true, will be replaced by a spinner.(`prefixClassName` and `prefixStyle` still work)
* */
prefix?: ReactElement;
prefixClassName?: string;
prefixStyle?: CSSProperties;
contentClassName?: string;
contentStyle?: CSSProperties;
type IconButtonProps = PropsWithChildren<BaseButtonProps> &
Omit<HTMLAttributes<HTMLDivElement>, 'type'>;
/**
* By default, it is considered as an icon with preset size and color,
* can be overridden by `suffixClassName` and `suffixStyle`.
* */
suffix?: ReactElement;
suffixClassName?: string;
suffixStyle?: CSSProperties;
const defaultProps = {
type: 'default',
disabled: false,
shape: 'default',
size: 'default',
iconPosition: 'start',
loading: false,
withoutHoverStyle: false,
} as const;
tooltip?: TooltipProps['content'];
tooltipOptions?: Partial<Omit<TooltipProps, 'content'>>;
}
const ButtonIcon: FC<IconButtonProps> = props => {
const {
size,
icon,
iconPosition = 'start',
children,
type,
loading,
withoutHoverStyle,
...otherProps
} = {
...defaultProps,
...props,
};
const onlyIcon = icon && !children;
return (
<div
{...otherProps}
className={clsx(buttonIcon, {
'color-white': type && type !== 'default' && type !== 'plain',
large: size === 'large',
extraLarge: size === 'extraLarge',
end: iconPosition === 'end' && !onlyIcon,
start: iconPosition === 'start' && !onlyIcon,
loading,
})}
data-without-hover={withoutHoverStyle}
>
{icon}
const IconSlot = ({
icon,
loading,
className,
...attrs
}: {
icon?: ReactElement;
loading?: boolean;
} & HTMLAttributes<HTMLElement>) => {
const showLoadingHere = loading !== undefined;
const visible = icon || loading;
return visible ? (
<div className={clsx(styles.icon, className)} {...attrs}>
{showLoadingHere && loading ? <Loading size="100%" /> : null}
{icon && !loading
? cloneElement(icon, {
width: '100%',
height: '100%',
...icon.props,
})
: null}
</div>
);
) : null;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const {
(
{
variant = 'secondary',
size = 'default',
children,
type,
disabled,
shape,
size,
icon: propsIcon,
iconPosition,
block,
loading,
withoutHoverStyle,
className,
prefix,
prefixClassName,
prefixStyle,
suffix,
suffixClassName,
suffixStyle,
contentClassName,
contentStyle,
tooltip,
tooltipOptions,
onClick,
...otherProps
} = {
...defaultProps,
...props,
} satisfies ButtonProps;
const icon = useMemo(() => {
if (loading) {
return <Loading />;
}
return propsIcon;
}, [propsIcon, loading]);
const baseIconButtonProps = useMemo(() => {
return {
size,
iconPosition,
icon,
type,
disabled,
loading,
} as const;
}, [disabled, icon, iconPosition, loading, size, type]);
},
ref
) => {
const handleClick = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (loading || disabled) return;
onClick?.(e);
},
[disabled, loading, onClick]
);
return (
<button
{...otherProps}
ref={ref}
className={clsx(
button,
{
primary: type === 'primary',
plain: type === 'plain',
error: type === 'error',
warning: type === 'warning',
success: type === 'success',
processing: type === 'processing',
large: size === 'large',
extraLarge: size === 'extraLarge',
disabled,
circle: shape === 'circle',
round: shape === 'round',
block,
loading,
'without-hover': withoutHoverStyle,
},
className
)}
disabled={disabled}
data-disabled={disabled}
>
{icon && iconPosition === 'start' ? (
<ButtonIcon
{...baseIconButtonProps}
{...props.componentProps?.startIcon}
icon={icon}
iconPosition="start"
<Tooltip content={tooltip} {...tooltipOptions}>
<button
{...otherProps}
ref={ref}
className={clsx(styles.button, className)}
data-loading={loading || undefined}
data-block={block || undefined}
disabled={disabled}
data-disabled={disabled || undefined}
data-size={size}
data-variant={variant}
onClick={handleClick}
>
<IconSlot
icon={prefix}
loading={loading}
className={prefixClassName}
style={prefixStyle}
/>
) : null}
<span>{children}</span>
{icon && iconPosition === 'end' ? (
<ButtonIcon
{...baseIconButtonProps}
{...props.componentProps?.endIcon}
icon={icon}
iconPosition="end"
{children ? (
<span
className={clsx(styles.content, contentClassName)}
style={contentStyle}
>
{children}
</span>
) : null}
<IconSlot
icon={suffix}
className={suffixClassName}
style={suffixStyle}
/>
) : null}
</button>
</button>
</Tooltip>
);
}
);
@@ -1,49 +1,153 @@
import { InformationIcon } from '@blocksuite/icons/rc';
import type { Meta, StoryFn } from '@storybook/react';
import { AfFiNeIcon } from '@blocksuite/icons/rc';
import type { Meta } from '@storybook/react';
import clsx from 'clsx';
import { type ReactElement, useCallback, useEffect, useState } from 'react';
import { Switch } from '../switch';
import * as styles from './button.stories.css';
import type { IconButtonProps } from './icon-button';
import { IconButton } from './icon-button';
export default {
title: 'UI/IconButton',
component: IconButton,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<IconButtonProps>;
const Template: StoryFn<IconButtonProps> = args => <IconButton {...args} />;
const types: IconButtonProps['variant'][] = ['plain', 'solid', 'danger'];
const sizes: IconButtonProps['size'][] = ['12', '14', '16', '20', '24'];
export const Plain: StoryFn<IconButtonProps> = Template.bind(undefined);
Plain.args = {
children: <InformationIcon />,
const Groups = ({
children,
...props
}: Omit<IconButtonProps, 'type' | 'size' | 'children'> & {
children?: ReactElement;
}) => {
return (
<table className={styles.table}>
<thead>
<tr>
<td>Type/Size</td>
{sizes.map(size => (
<td key={size}>{size}</td>
))}
</tr>
</thead>
<tbody>
{types.map(type => (
<tr key={type}>
<td>{type}</td>
{sizes.map(size => (
<td key={size}>
<IconButton variant={type} size={size} {...props}>
{children ?? <AfFiNeIcon />}
</IconButton>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export const Primary: StoryFn<IconButtonProps> = Template.bind(undefined);
Primary.args = {
type: 'primary',
icon: <InformationIcon />,
export const Default = () => <Groups />;
export const Loading = () => {
const [loading, setLoading] = useState(false);
const toggleLoading = useCallback(() => setLoading(v => !v), []);
useEffect(() => {
setInterval(toggleLoading, 1000);
}, [toggleLoading]);
return <Groups loading={loading} />;
};
export const Disabled: StoryFn<IconButtonProps> = Template.bind(undefined);
Disabled.args = {
disabled: true,
icon: <InformationIcon />,
export const OverrideViaClassName = () => {
const [overrideBg, setOverrideBg] = useState(false);
const [overrideBorder, setOverrideBorder] = useState(false);
const [overridePrefixColor, setOverridePrefixColor] = useState(false);
return (
<div>
<div className={styles.settings}>
<section>
<span>Override background color</span>
<Switch checked={overrideBg} onChange={setOverrideBg} />
</section>
<section>
<span>Override border color</span>
<Switch checked={overrideBorder} onChange={setOverrideBorder} />
</section>
<section>
<span>Override icon color</span>
<Switch
checked={overridePrefixColor}
onChange={setOverridePrefixColor}
/>
</section>
</div>
<Groups
className={clsx({
[styles.overrideBackground]: overrideBg,
[styles.overrideBorder]: overrideBorder,
})}
iconClassName={clsx({
[styles.overrideIconColor]: overridePrefixColor,
})}
/>
</div>
);
};
export const ExtraSmallSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
ExtraSmallSizeButton.args = {
size: 'extraSmall',
icon: <InformationIcon />,
};
export const SmallSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
SmallSizeButton.args = {
size: 'small',
icon: <InformationIcon />,
};
export const LargeSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
LargeSizeButton.args = {
size: 'large',
icon: <InformationIcon />,
export const CustomSize = () => {
const sizes = [
[13, 2],
[15, 2],
[17, 2],
[19, 2],
[21, 3],
[23, 3],
[25, 3],
[27, 3],
[29, 4],
[31, 4],
[33, 4],
[35, 4],
];
return types.map(type => {
return (
<div key={type}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
{sizes.map(size => (
<div
key={size[0]}
style={{
fontSize: 10,
textAlign: 'center',
color: 'rgba(100, 100, 100, 0.5)',
}}
>
<IconButton
size={size[0]}
style={{ padding: size[1] }}
variant={type}
>
<AfFiNeIcon />
</IconButton>
<div style={{ marginTop: 8 }}>Size: {size[0]}px</div>
<div style={{ marginTop: 2 }}>Padding: {size[1]}px</div>
</div>
))}
</div>
</div>
);
});
};
export const Disabled = () => <Groups disabled />;
@@ -1,85 +1,78 @@
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import { type CSSProperties, forwardRef, type ReactElement } from 'react';
import { Loading } from '../loading';
import type { ButtonType } from './button';
import { iconButton } from './button.css';
import { Button, type ButtonProps } from './button';
import { iconButton, iconSizeVar } from './button.css';
export type IconButtonSize = 'default' | 'large' | 'small' | 'extraSmall';
export type IconButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'type'> &
PropsWithChildren<{
type?: ButtonType;
disabled?: boolean;
size?: IconButtonSize;
loading?: boolean;
withoutPadding?: boolean;
active?: boolean;
withoutHoverStyle?: boolean;
icon?: ReactElement;
}>;
const defaultProps = {
type: 'plain',
disabled: false,
size: 'default',
loading: false,
withoutPadding: false,
active: false,
withoutHoverStyle: false,
} as const;
export interface IconButtonProps
extends Omit<
ButtonProps,
| 'variant'
| 'size'
| 'prefix'
| 'suffix'
| 'children'
| 'prefixClassName'
| 'prefixStyle'
| 'suffix'
| 'suffixClassName'
| 'suffixStyle'
> {
/** Icon element */
children?: ReactElement;
/** Same as `children`, compatibility of the old API */
icon?: ReactElement;
variant?: 'plain' | 'solid' | 'danger' | 'custom';
/**
* Use preset size,
* or use custom size(px) (default padding is `2px`, have to override yourself)
*
* > These presets size are referenced from the design system.
* > The number is the size of the icon, the button size is calculated based on the icon size + padding.
* > OR, you can define `width` and `height` in `style` or `className` directly.
*/
size?: '12' | '14' | '16' | '20' | '24' | number;
iconClassName?: string;
iconStyle?: CSSProperties;
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(props, ref) => {
const {
type,
size,
withoutPadding,
children,
disabled,
loading,
active,
withoutHoverStyle,
icon: propsIcon,
(
{
variant = 'plain',
size = '20',
style,
className,
children,
icon,
iconClassName,
iconStyle,
...otherProps
} = {
...defaultProps,
...props,
};
},
ref
) => {
const validatedSize = isNaN(parseInt(size as string, 10)) ? 16 : size;
return (
<button
<Button
ref={ref}
className={clsx(
iconButton,
{
'without-padding': withoutPadding,
primary: type === 'primary',
plain: type === 'plain',
error: type === 'error',
warning: type === 'warning',
success: type === 'success',
processing: type === 'processing',
large: size === 'large',
small: size === 'small',
'extra-small': size === 'extraSmall',
disabled,
loading,
active,
'without-hover': withoutHoverStyle,
},
className
)}
disabled={disabled}
data-disabled={disabled}
style={{
...style,
...assignInlineVars({
[iconSizeVar]: `${validatedSize}px`,
}),
}}
data-icon-variant={variant}
data-icon-size={validatedSize}
className={clsx(iconButton, className)}
size={'custom'}
variant={'custom'}
prefix={children ?? icon}
prefixClassName={iconClassName}
prefixStyle={iconStyle}
{...otherProps}
>
{loading ? <Loading /> : children || propsIcon}
</button>
/>
);
}
);
@@ -1,26 +0,0 @@
import type {
CSSProperties,
HTMLAttributes,
PropsWithChildren,
ReactElement,
} from 'react';
export const SIZE_SMALL = 'small' as const;
export const SIZE_MIDDLE = 'middle' as const;
export const SIZE_DEFAULT = 'default' as const;
export type ButtonProps = PropsWithChildren &
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
size?: typeof SIZE_SMALL | typeof SIZE_MIDDLE | typeof SIZE_DEFAULT;
disabled?: boolean;
hoverBackground?: CSSProperties['background'];
hoverColor?: CSSProperties['color'];
hoverStyle?: CSSProperties;
icon?: ReactElement;
iconPosition?: 'start' | 'end';
shape?: 'default' | 'round' | 'circle';
type?: 'primary' | 'light' | 'warning' | 'danger' | 'default';
bold?: boolean;
loading?: boolean;
noBorder?: boolean;
};
@@ -1,89 +0,0 @@
import type { ButtonProps } from './interface';
export const getButtonColors = (
type: ButtonProps['type'],
disabled: boolean,
extend?: {
hoverBackground: ButtonProps['hoverBackground'];
hoverColor: ButtonProps['hoverColor'];
hoverStyle: ButtonProps['hoverStyle'];
}
) => {
switch (type) {
case 'primary':
return {
background: 'var(--affine-primary-color)',
color: 'var(--affine-white)',
borderColor: 'var(--affine-primary-color)',
backgroundBlendMode: 'overlay',
opacity: disabled ? '.4' : '1',
'.affine-button-icon': {
color: 'var(--affine-white)',
},
':hover': {
background:
'linear-gradient(var(--affine-primary-color),var(--affine-primary-color)),var(--affine-hover-color)',
},
};
case 'light':
return {
background: 'var(--affine-tertiary-color)',
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-text-emphasis-color)',
borderColor: 'var(--affine-tertiary-color)',
'.affine-button-icon': {
borderColor: 'var(--affine-text-emphasis-color)',
},
':hover': {
borderColor: disabled
? 'var(--affine-disable-color)'
: 'var(--affine-text-emphasis-color)',
},
};
case 'warning':
return {
background: 'var(--affine-background-warning-color)',
color: 'var(--affine-warning-color)',
borderColor: 'var(--affine-background-warning-color)',
'.affine-button-icon': {
color: 'var(--affine-warning-color)',
},
':hover': {
borderColor: 'var(--affine-warning-color)',
color: extend?.hoverColor,
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
};
case 'danger':
return {
background: 'var(--affine-background-error-color)',
color: 'var(--affine-error-color)',
borderColor: 'var(--affine-background-error-color)',
'.affine-button-icon': {
color: 'var(--affine-error-color)',
},
':hover': {
borderColor: 'var(--affine-error-color)',
color: extend?.hoverColor,
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
};
default:
return {
color: 'var(--affine-text-primary-color)',
borderColor: 'var(--affine-border-color)',
':hover': {
borderColor: 'var(--affine-primary-color)',
color: extend?.hoverColor ?? 'var(--affine-primary-color)',
'.affine-button-icon': {
color: extend?.hoverColor ?? 'var(--affine-primary-color)',
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
},
};
}
};
@@ -134,8 +134,7 @@ export const NavButtons = memo(function NavButtons({
<div className={styles.headerNavButtons} key="nav-btn-group">
<IconButton
key="nav-btn-prev"
size="small"
className={styles.focusInteractive}
size="16"
disabled={prevDisabled}
data-testid="date-picker-nav-prev"
onClick={onPrev}
@@ -147,8 +146,7 @@ export const NavButtons = memo(function NavButtons({
<IconButton
key="nav-btn-next"
size="small"
className={styles.focusInteractive}
size="16"
disabled={nextDisabled}
data-testid="date-picker-nav-next"
onClick={onNext}
@@ -1,9 +1,10 @@
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { withUnit } from '../../utils/with-unit';
import { loading, speedVar } from './styles.css';
export interface LoadingProps {
size?: number;
size?: number | string;
speed?: number;
progress?: number;
}
@@ -13,11 +14,13 @@ export const Loading = ({
speed = 1.2,
progress = 0.2,
}: LoadingProps) => {
// allow `string` such as `16px` | `100%` | `1em`
const sizeWithUnit = size ? withUnit(size, 'px') : '16px';
return (
<svg
className={loading}
width={size ? `${size}px` : '16px'}
height={size ? `${size}px` : '16px'}
width={sizeWithUnit}
height={sizeWithUnit}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -61,7 +61,7 @@ const ConfirmModalTemplate: StoryFn<ConfirmModalProps> = () => {
confirmText="Confirm"
confirmButtonOptions={{
loading: loading,
type: 'primary',
variant: 'primary',
}}
>
<Input placeholder="input someting" status={inputStatus} />
@@ -82,7 +82,7 @@ const OverlayModalTemplate: StoryFn<OverlayModalProps> = () => {
title="Modal Title"
description="Modal description"
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
topImage={
<div
@@ -130,10 +130,12 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
style: overlayStyle,
...otherOverlayOptions
} = {},
closeButtonOptions = {},
closeButtonOptions,
children,
...otherProps
} = props;
const { className: closeButtonClassName, ...otherCloseButtonProps } =
closeButtonOptions || {};
const [container, setContainer] = useState<ModalTransitionContainer | null>(
null
@@ -204,11 +206,11 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
{withoutCloseButton ? null : (
<Dialog.Close asChild>
<IconButton
className={styles.closeButton}
size="20"
className={clsx(styles.closeButton, closeButtonClassName)}
aria-label="Close"
type="plain"
data-testid="modal-close-button"
{...closeButtonOptions}
{...otherCloseButtonProps}
>
<CloseIcon />
</IconButton>
@@ -32,7 +32,7 @@ export const AdminPanelHeader = ({
</div>
<div>
<Button
type="primary"
variant="primary"
disabled={modifiedValues.length === 0}
onClick={() => {
openConfirmModal({
@@ -41,7 +41,7 @@ export const AdminPanelHeader = ({
'Are you sure you want to save the following changes?',
confirmText: 'Save',
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
onConfirm: onConfirm,
children:
@@ -81,7 +81,7 @@ export const ErrorDetail: FC<ErrorDetailProps> = props => {
))}
<div className={styles.errorFooter}>
<Button
type="primary"
variant="primary"
onClick={onBtnClick}
loading={isBtnLoading}
size="extraLarge"
@@ -21,7 +21,6 @@ export const thumbContent = style({
});
export const actionButton = style({
fontWeight: 500,
fontSize: cssVar('fontSm'),
lineHeight: '22px',
});
@@ -103,7 +103,7 @@ export const AIOnboardingEdgeless = () => {
notify.dismiss(id);
toggleEdgelessAIOnboarding(false);
}}
type="plain"
variant="plain"
className={styles.actionButton}
>
<span className={styles.getStartedButtonText}>
@@ -113,7 +113,7 @@ export const AIOnboardingEdgeless = () => {
{aiSubscription ? null : (
<Button
className={styles.actionButton}
type="plain"
variant="plain"
onClick={() => {
goToPricingPlans();
notify.dismiss(id);
@@ -114,17 +114,3 @@ export const subscribeActions = style({
gap: 12,
alignItems: 'center',
});
export const baseActionButton = style({
fontSize: cssVar('fontBase'),
selectors: {
'&.large': {
fontWeight: 500,
},
},
});
export const transparentActionButton = style([
baseActionButton,
{
backgroundColor: 'transparent',
},
]);
@@ -243,36 +243,26 @@ export const AIOnboardingGeneral = () => {
>
{isLast ? (
<>
<IconButton
size="default"
icon={<ArrowLeftSmallIcon width={20} height={20} />}
onClick={onPrev}
type="plain"
className={styles.baseActionButton}
/>
<IconButton size="20" onClick={onPrev}>
<ArrowLeftSmallIcon />
</IconButton>
{aiSubscription ? (
<Button
className={styles.baseActionButton}
size="large"
onClick={closeAndDismiss}
type="primary"
variant="primary"
>
{t['com.affine.ai-onboarding.general.get-started']()}
</Button>
) : (
<div className={styles.subscribeActions}>
<Button
className={styles.baseActionButton}
size="large"
onClick={goToPricingPlans}
>
<Button size="large" onClick={goToPricingPlans}>
{t['com.affine.ai-onboarding.general.purchase']()}
</Button>
<Button
className={styles.baseActionButton}
size="large"
onClick={closeAndDismiss}
type="primary"
variant="primary"
>
{t['com.affine.ai-onboarding.general.try-for-free']()}
</Button>
@@ -282,21 +272,15 @@ export const AIOnboardingGeneral = () => {
) : (
<>
{isFirst ? (
<Button
className={styles.transparentActionButton}
onClick={remindLater}
size="large"
type="default"
>
<Button onClick={remindLater} size="large">
{t['com.affine.ai-onboarding.general.skip']()}
</Button>
) : (
<Button
icon={<ArrowLeftSmallIcon />}
className={styles.baseActionButton}
prefix={<ArrowLeftSmallIcon />}
onClick={onPrev}
type="plain"
size="large"
variant="plain"
>
{t['com.affine.ai-onboarding.general.prev']()}
</Button>
@@ -305,12 +289,7 @@ export const AIOnboardingGeneral = () => {
<div>
{index + 1} / {list.length}
</div>
<Button
className={styles.baseActionButton}
size="large"
type="primary"
onClick={onNext}
>
<Button size="large" variant="primary" onClick={onNext}>
{t['com.affine.ai-onboarding.general.next']()}
</Button>
</div>
@@ -41,7 +41,7 @@ const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
<Button
className={styles.actionButton}
type="plain"
variant="plain"
onClick={onDismiss}
>
{t['com.affine.ai-onboarding.local.action-learn-more']()}
@@ -50,7 +50,7 @@ const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
{loggedIn ? null : (
<Button
className={styles.actionButton}
type="plain"
variant="plain"
onClick={() => {
onDismiss();
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });
@@ -99,7 +99,7 @@ export const AfterSignInSendEmail = ({
<Button
style={!verifyToken ? { cursor: 'not-allowed' } : {}}
disabled={!verifyToken || isSending}
type="plain"
variant="plain"
size="large"
onClick={onResendClick}
>
@@ -91,7 +91,7 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
<Button
style={!verifyToken ? { cursor: 'not-allowed' } : {}}
disabled={!verifyToken || isSending}
type="plain"
variant="plain"
size="large"
onClick={onResendClick}
>
@@ -27,7 +27,7 @@ export const AiLoginRequiredModal = () => {
},
confirmText: t['com.affine.ai.login-required.dialog-confirm'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText: t['com.affine.ai.login-required.dialog-cancel'](),
onOpenChange: setOpen,
@@ -82,11 +82,11 @@ function OAuthProvider({
return (
<Button
key={provider}
type="primary"
variant="primary"
block
size="extraLarge"
style={{ marginTop: 30 }}
icon={icon}
style={{ marginTop: 30, width: '100%' }}
prefix={icon}
onClick={onClick}
>
Continue with {provider}
@@ -203,7 +203,7 @@ export const SendEmail = ({
</Wrapper>
<Button
type="primary"
variant="primary"
size="extraLarge"
style={{ width: '100%' }}
disabled={hasSentEmail}
@@ -129,7 +129,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
</div>
<Button
data-testid="sign-in-button"
type="primary"
variant="primary"
size="extraLarge"
style={{ width: '100%' }}
disabled={isLoading}
@@ -5,8 +5,9 @@ import { authAtom } from '@affine/core/atoms';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel';
import { Trans, useI18n } from '@affine/i18n';
import { ArrowDownBigIcon } from '@blocksuite/icons/rc';
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useAtomValue } from 'jotai';
import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react';
@@ -149,21 +150,13 @@ export const SignIn: FC<AuthPanelProps> = ({
{verifyToken ? (
<Button
style={{ width: '100%' }}
size="extraLarge"
data-testid="continue-login-button"
block
loading={isMutating}
icon={
<ArrowDownBigIcon
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
}
iconPosition="end"
suffix={<ArrowRightBigIcon />}
suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
onClick={onContinue}
>
{t['com.affine.auth.sign.email.continue']()}
@@ -106,7 +106,7 @@ const NameWorkspaceContent = ({
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
confirmText={t['com.affine.nameWorkspace.button.create']()}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
disabled: !workspaceName || loading,
['data-testid' as string]: 'create-workspace-create-button',
}}
@@ -28,7 +28,7 @@ export const HistoryTipsModal = () => {
description={t['com.affine.history-vision.tips-modal.description']()}
cancelText={t['com.affine.history-vision.tips-modal.cancel']()}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
onConfirm={handleConfirm}
confirmText={t['com.affine.history-vision.tips-modal.confirm']()}
@@ -27,7 +27,7 @@ export const IssueFeedbackModal = () => {
to={`${runtimeConfig.githubUrl}/issues/new/choose`}
confirmText={t['com.affine.issue-feedback.confirm']()}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
external
/>
@@ -19,7 +19,7 @@ export const AnimateInTooltip = ({
</div>
<div className={styles.next}>
{visible ? (
<Button type="primary" size="extraLarge" onClick={onNext}>
<Button variant="primary" size="extraLarge" onClick={onNext}>
Next
</Button>
) : null}
@@ -235,7 +235,7 @@ export const EdgelessSwitch = ({
onSwitchToPageMode={onSwitchToPageMode}
onSwitchToEdgelessMode={onSwitchToEdgelessMode}
/>
<Button size="extraLarge" type="primary" onClick={onNextClick}>
<Button size="extraLarge" variant="primary" onClick={onNextClick}>
Next
</Button>
</header>
@@ -263,7 +263,7 @@ export const EdgelessSwitch = ({
<Button
className={styles.wellDoneEnterAnim}
onClick={onNextClick}
type="primary"
variant="primary"
size="extraLarge"
style={{ marginTop: 40 }}
>
@@ -251,11 +251,9 @@ const PlanPrompt = () => {
: '' /* TODO(@catsjuice): loading UI */
}
<IconButton
size="small"
icon={<CloseIcon />}
onClick={closeFreePlanPrompt}
/>
<IconButton onClick={closeFreePlanPrompt}>
<CloseIcon />
</IconButton>
</div>
);
}, [closeFreePlanPrompt, isProWorkspace, t]);
@@ -393,7 +391,7 @@ const PageHistoryList = ({
})}
{onLoadMore ? (
<Button
type="plain"
variant="plain"
loading={loadingMore}
disabled={loadingMore}
className={styles.historyItemLoadMore}
@@ -480,7 +478,7 @@ const PageHistoryManager = ({
},
confirmText: t['com.affine.history.confirm-restore-modal.restore'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
['data-testid' as string]: 'confirm-restore-history-button',
},
onConfirm: handleRestore,
@@ -520,12 +518,12 @@ const PageHistoryManager = ({
) : null}
<div className={styles.historyFooter}>
<Button type="plain" onClick={onClose}>
<Button onClick={onClose}>
{t['com.affine.history.back-to-page']()}
</Button>
<div className={styles.spacer} />
<Button
type="primary"
variant="primary"
onClick={onConfirmRestore}
disabled={isMutating || !activeVersion}
>
@@ -50,7 +50,7 @@ export const ConfirmDeletePropertyModal = ({
onClick: onCancel,
}}
confirmButtonOptions={{
type: 'error',
variant: 'error',
}}
/>
);
@@ -117,22 +117,14 @@ export const tableBodySortable = style({
});
export const addPropertyButton = style({
display: 'flex',
alignItems: 'center',
alignSelf: 'flex-start',
fontSize: cssVar('fontSm'),
color: `${cssVar('textSecondaryColor')} !important`,
color: `${cssVar('textSecondaryColor')}`,
padding: '0 4px',
height: 36,
cursor: 'pointer',
':hover': {
color: cssVar('textPrimaryColor'),
backgroundColor: cssVar('hoverColor'),
},
gap: 2,
fontWeight: 400,
gap: 6,
});
globalStyle(`${addPropertyButton} svg`, {
fontSize: 16,
color: cssVar('iconSecondary'),
@@ -703,11 +703,9 @@ export const PagePropertiesTableHeader = ({
</div>
{properties.length === 0 || manager.readonly ? null : (
<PagePropertiesSettingsPopup>
<IconButton
data-testid="page-info-show-more"
type="plain"
icon={<MoreHorizontalIcon />}
/>
<IconButton data-testid="page-info-show-more" size="20">
<MoreHorizontalIcon />
</IconButton>
</PagePropertiesSettingsPopup>
)}
<Collapsible.Trigger asChild role="button" onClick={handleCollapse}>
@@ -715,15 +713,12 @@ export const PagePropertiesTableHeader = ({
className={styles.tableHeaderCollapseButtonWrapper}
data-testid="page-info-collapse"
>
<IconButton
type="plain"
icon={
<ToggleExpandIcon
className={styles.collapsedIcon}
data-collapsed={!open}
/>
}
/>
<IconButton size="20">
<ToggleExpandIcon
className={styles.collapsedIcon}
data-collapsed={!open}
/>
</IconButton>
</div>
</Collapsible.Trigger>
</div>
@@ -1056,8 +1051,8 @@ export const PagePropertiesAddProperty = () => {
return (
<Menu {...menuOptions}>
<Button
type="plain"
icon={<PlusIcon />}
variant="plain"
prefix={<PlusIcon />}
className={styles.addPropertyButton}
>
{t['com.affine.page-properties.add-property']()}
@@ -405,11 +405,9 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
<TagItem maxWidth="100%" tag={tag} mode="inline" />
<div className={styles.spacer} />
<EditTagMenu tagId={tag.id} onTagDelete={onTagDelete}>
<IconButton
className={styles.tagEditIcon}
type="plain"
icon={<MoreHorizontalIcon />}
/>
<IconButton className={styles.tagEditIcon}>
<MoreHorizontalIcon />
</IconButton>
</EditTagMenu>
</div>
);
@@ -112,7 +112,7 @@ export const CloudQuotaModal = () => {
isFreePlanOwner ? t['com.affine.payment.upgrade']() : t['Got it']()
}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
/>
);
@@ -37,7 +37,7 @@ export const LocalQuotaModal = () => {
onConfirm={onConfirm}
confirmText={t['Got it']()}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
/>
);
@@ -132,7 +132,7 @@ export const AIUsagePanel = () => {
</div>
{hasPaymentFeature && (
<AISubscribe type="primary" className={styles.storageButton}>
<AISubscribe variant="primary">
{t['com.affine.payment.ai.usage.purchase-button-label']()}
</AISubscribe>
)}
@@ -142,7 +142,6 @@ export const AvatarAndName = () => {
<Button
data-testid="save-user-name"
onClick={handleUpdateUserName}
className={styles.button}
style={{
marginLeft: '12px',
}}
@@ -234,7 +233,7 @@ export const AccountSetting: FC = () => {
/>
<AvatarAndName />
<SettingRow name={t['com.affine.settings.email']()} desc={account.email}>
<Button onClick={onChangeEmail} className={styles.button}>
<Button onClick={onChangeEmail}>
{account.info?.emailVerified
? t['com.affine.settings.email.action.change']()
: t['com.affine.settings.email.action.verify']()}
@@ -244,7 +243,7 @@ export const AccountSetting: FC = () => {
name={t['com.affine.settings.password']()}
desc={t['com.affine.settings.password.message']()}
>
<Button onClick={onPasswordButtonClick} className={styles.button}>
<Button onClick={onPasswordButtonClick}>
{account.info?.hasPassword
? t['com.affine.settings.password.action.change']()
: t['com.affine.settings.password.action.set']()}
@@ -28,6 +28,3 @@ globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, {
export const storageProgressBar = style({
height: '100%',
});
export const storageButton = style({
padding: '4px 12px',
});
@@ -18,7 +18,7 @@ export interface StorageProgressProgress {
enum ButtonType {
Primary = 'primary',
Default = 'default',
Default = 'secondary',
}
export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
@@ -101,11 +101,7 @@ export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
<Button variant={buttonType} onClick={onUpgrade}>
{isFreeUser
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
@@ -39,6 +39,3 @@ globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
color: cssVar('white'),
fontSize: cssVar('fontH4'),
});
export const button = style({
padding: '4px 12px',
});
@@ -299,9 +299,7 @@ const TypeFormLink = () => {
desc={t['com.affine.payment.billing-type-form.description']()}
>
<a target="_blank" href={link} rel="noreferrer">
<Button style={{ padding: '4px 12px' }}>
{t['com.affine.payment.billing-type-form.go']()}
</Button>
<Button>{t['com.affine.payment.billing-type-form.go']()}</Button>
</a>
</SettingRow>
);
@@ -435,7 +433,7 @@ const PlanAction = ({
return (
<Button
className={styles.planAction}
type="primary"
variant="primary"
onClick={gotoPlansSetting}
>
{plan === SubscriptionPlan.Pro
@@ -460,12 +458,7 @@ const PaymentMethodUpdater = () => {
}, [trigger]);
return (
<Button
className={styles.button}
onClick={update}
loading={isMutating}
disabled={isMutating}
>
<Button onClick={update} loading={isMutating} disabled={isMutating}>
{t['com.affine.payment.billing-setting.update']()}
</Button>
);
@@ -492,7 +485,7 @@ const ResumeSubscription = () => {
return (
<ResumeAction open={open} onOpenChange={setOpen}>
<Button className={styles.button} onClick={handleClick}>
<Button onClick={handleClick}>
{t['com.affine.payment.billing-setting.resume-subscription']()}
</Button>
</ResumeAction>
@@ -503,10 +496,11 @@ const CancelSubscription = ({ loading }: { loading?: boolean }) => {
return (
<IconButton
style={{ pointerEvents: 'none' }}
icon={<ArrowRightSmallIcon />}
disabled={loading}
loading={loading}
/>
>
<ArrowRightSmallIcon />
</IconButton>
);
};
@@ -583,7 +577,7 @@ const InvoiceLine = ({
: ''
} $${invoice.amount / 100} - ${planText}`}
>
<Button className={styles.button} onClick={open}>
<Button onClick={open}>
{t['com.affine.payment.billing-setting.view-invoice']()}
</Button>
</SettingRow>
@@ -46,9 +46,6 @@ export const currentPlanName = style({
color: cssVar('textEmphasisColor'),
cursor: 'pointer',
});
export const button = style({
padding: '4px 12px',
});
export const subscriptionSettingSkeleton = style({
display: 'flex',
flexDirection: 'column',
@@ -63,7 +63,7 @@ const ExperimentalFeaturesPrompt = ({
<Button
disabled={!checked}
onClick={onConfirm}
type="primary"
variant="primary"
data-testid="experimental-confirm-button"
>
{t[
@@ -43,12 +43,12 @@ export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
confirmText:
t['com.affine.payment.ai.action.cancel.confirm.confirm-text'](),
confirmButtonOptions: {
type: 'default',
variant: 'secondary',
},
cancelText:
t['com.affine.payment.ai.action.cancel.confirm.cancel-text'](),
cancelButtonOptions: {
type: 'primary',
variant: 'primary',
},
onConfirm: async () => {
try {
@@ -92,7 +92,12 @@ export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
]);
return (
<Button onClick={cancel} loading={isMutating} type="primary" {...btnProps}>
<Button
onClick={cancel}
loading={isMutating}
variant="primary"
{...btnProps}
>
{t['com.affine.payment.ai.action.cancel.button-label']()}
</Button>
);
@@ -16,7 +16,7 @@ export const AILogin = (btnProps: ButtonProps) => {
}, [setOpen]);
return (
<Button onClick={onClickSignIn} type="primary" {...btnProps}>
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>
{t['com.affine.payment.ai.action.login.button-label']()}
</Button>
);
@@ -48,7 +48,7 @@ export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
confirmText:
t['com.affine.payment.ai.action.resume.confirm.confirm-text'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText:
t['com.affine.payment.ai.action.resume.confirm.cancel-text'](),
@@ -79,7 +79,12 @@ export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
}, [subscription, openConfirmModal, t, module, idempotencyKey]);
return (
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>
<Button
loading={isMutating}
onClick={resume}
variant="primary"
{...btnProps}
>
{t['com.affine.payment.ai.action.resume.button-label']()}
</Button>
);
@@ -102,7 +102,7 @@ export const AISubscribe = ({
<Button
loading={isMutating}
onClick={subscribe}
type="primary"
variant="primary"
{...btnProps}
>
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
@@ -46,6 +46,7 @@ export const allPlansLink = style({
export const collapsibleHeader = style({
display: 'flex',
alignItems: 'start',
marginBottom: 8,
});
export const collapsibleHeaderContent = style({
@@ -58,7 +58,7 @@ export const PricingCollapsible = ({
<div className={styles.collapsibleHeaderTitle}>{title}</div>
<div className={styles.collapsibleHeaderCaption}>{caption}</div>
</div>
<IconButton onClick={toggle}>
<IconButton onClick={toggle} size="20">
<ArrowUpSmallIcon
style={{
transform: open ? 'rotate(0deg)' : 'rotate(180deg)',
@@ -163,7 +163,7 @@ export const PlanLayout = ({ cloud, ai, cloudTip }: PlanLayoutProps) => {
</div>
<Button
onClick={() => scrollToAnchor('cloudPricingPlan')}
type="primary"
variant="primary"
>
{t['com.affine.ai-scroll-tip.view']()}
</Button>
@@ -48,7 +48,7 @@ export const ConfirmLoadingModal = ({
cancelText={cancelText}
confirmText={confirmText}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
loading,
}}
open={open}
@@ -120,7 +120,7 @@ export const DowngradeModal = ({
<Button
disabled={loading}
onClick={() => onOpenChange?.(false)}
type="primary"
variant="primary"
>
{t['com.affine.payment.modal.downgrade.confirm']()}
</Button>
@@ -1,4 +1,4 @@
import { Button } from '@affine/component/ui/button';
import { Button, type ButtonProps } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip';
import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
@@ -10,10 +10,11 @@ import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n';
import { DoneIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { authAtom } from '../../../../../atoms/index';
@@ -194,7 +195,7 @@ const Downgrade = ({ disabled }: { disabled?: boolean }) => {
<div className={styles.planAction}>
<Button
className={styles.planAction}
type="primary"
variant="primary"
onClick={handleClick}
disabled={disabled}
>
@@ -232,7 +233,7 @@ const BookDemo = ({ plan }: { plan: SubscriptionPlan }) => {
});
}}
>
<Button className={styles.planAction} type="primary">
<Button className={styles.planAction} variant="primary">
{t['com.affine.payment.tell-us-use-case']()}
</Button>
</a>
@@ -243,8 +244,8 @@ export const Upgrade = ({
className,
recurring,
children,
...attrs
}: HTMLAttributes<HTMLButtonElement> & {
...btnProps
}: ButtonProps & {
recurring: SubscriptionRecurring;
}) => {
const [isMutating, setMutating] = useState(false);
@@ -307,11 +308,11 @@ export const Upgrade = ({
return (
<Button
className={clsx(styles.planAction, className)}
type="primary"
variant="primary"
onClick={upgrade}
disabled={isMutating}
loading={isMutating}
{...attrs}
{...btnProps}
>
{children ?? t['com.affine.payment.upgrade']()}
</Button>
@@ -371,7 +372,7 @@ const ChangeRecurring = ({
<>
<Button
className={styles.planAction}
type="primary"
variant="primary"
onClick={onStartChange}
disabled={disabled || isMutating}
loading={isMutating}
@@ -405,7 +406,7 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
<Button
onClick={onClickSignIn}
className={styles.planAction}
type="primary"
variant="primary"
>
{children}
</Button>
@@ -415,7 +416,6 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
const ResumeButton = () => {
const t = useI18n();
const [open, setOpen] = useState(false);
const [hovered, setHovered] = useState(false);
const subscription = useService(SubscriptionService).subscription;
const handleClick = useCallback(() => {
@@ -435,14 +435,14 @@ const ResumeButton = () => {
return (
<ResumeAction open={open} onOpenChange={setOpen}>
<Button
className={styles.planAction}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={styles.resumeAction}
onClick={handleClick}
style={assignInlineVars({
'--default-content': t['com.affine.payment.current-plan'](),
'--hover-content': t['com.affine.payment.resume-renewal'](),
})}
>
{hovered
? t['com.affine.payment.resume-renewal']()
: t['com.affine.payment.current-plan']()}
<span className={styles.resumeActionContent} />
</Button>
</ResumeAction>
);
@@ -177,7 +177,17 @@ export const planPriceDesc = style({
});
export const planAction = style({
width: '100%',
fontWeight: 500,
});
export const resumeAction = style([planAction, {}]);
export const resumeActionContent = style({
':after': {
content: 'var(--default-content)',
},
selectors: {
[`${resumeAction}:hover &:after`]: {
content: 'var(--hover-content)',
},
},
});
export const planBenefits = style({
fontSize: cssVar('fontXs'),
@@ -38,7 +38,7 @@ export const WorkspaceDeleteModal = ({
cancelText={t['com.affine.workspaceDelete.button.cancel']()}
confirmText={t['com.affine.workspaceDelete.button.delete']()}
confirmButtonOptions={{
type: 'error',
variant: 'error',
disabled: !allowDelete,
['data-testid' as string]: 'delete-workspace-confirm-button',
}}
@@ -136,7 +136,7 @@ export const DeleteLeaveWorkspace = () => {
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
confirmText={t['Leave']()}
confirmButtonOptions={{
type: 'warning',
variant: 'error',
}}
/>
)}
@@ -56,7 +56,7 @@ export const EnableCloudPanel = () => {
>
<Button
data-testid="publish-enable-affine-cloud-button"
type="primary"
variant="primary"
onClick={confirmEnableCloudAndClose}
style={{ marginTop: '12px' }}
>
@@ -63,7 +63,7 @@ const MembersPanelLocal = () => {
<Tooltip content={t['com.affine.settings.member-tooltip']()}>
<div className={style.fakeWrapper}>
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
<Button size="large">{t['Invite Members']()}</Button>
<Button>{t['Invite Members']()}</Button>
</SettingRow>
</div>
</Tooltip>
@@ -393,7 +393,6 @@ const MemberItem = ({
>
<IconButton
disabled={!operationButtonInfo.show}
type="plain"
style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0,
@@ -173,11 +173,9 @@ const EditPropertyButton = ({
}}
items={editing ? editMenuItems : defaultMenuItems}
>
<IconButton
onClick={() => setOpen(true)}
type="plain"
icon={<MoreHorizontalIcon />}
/>
<IconButton onClick={() => setOpen(true)} size="20">
<MoreHorizontalIcon />
</IconButton>
</Menu>
<ConfirmDeletePropertyModal
onConfirm={() => {
@@ -350,7 +348,7 @@ const WorkspaceSettingPropertiesMain = () => {
<div className={styles.listHeader}>
{properties.length > 0 ? (
<Menu items={filterMenuItems}>
<Button type="default" icon={<FilterIcon />}>
<Button prefix={<FilterIcon />}>
{filterMode === 'all'
? t['com.affine.filter']()
: t[`com.affine.settings.workspace.properties.${filterMode}`]()}
@@ -365,7 +363,7 @@ const WorkspaceSettingPropertiesMain = () => {
/>
}
>
<Button type="primary">
<Button variant="primary">
{t['com.affine.settings.workspace.properties.add_property']()}
</Button>
</Menu>
@@ -55,7 +55,7 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
}, [shareService]);
return (
<Button ref={ref} className={styles.shareButton} type="primary">
<Button ref={ref} className={styles.shareButton} variant="primary">
{shared
? t['com.affine.share-menu.sharedButton']()
: t['com.affine.share-menu.shareButton']()}
@@ -40,7 +40,7 @@ export const LocalSharePage = (props: ShareMenuProps) => {
<div>
<Button
onClick={props.onEnableAffineCloud}
type="primary"
variant="primary"
data-testid="share-menu-enable-affine-cloud-button"
>
{t['Enable AFFiNE Cloud']()}
@@ -256,7 +256,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
) : (
<Button
onClick={onClickCreateLink}
type="primary"
variant="primary"
data-testid="share-menu-create-link-button"
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
>
@@ -32,7 +32,7 @@ export const SignOutModal = ({ ...props }: ConfirmModalProps) => {
cancelText={cancelText ?? defaultTexts.cancelText}
confirmText={confirmText ?? defaultTexts.children}
confirmButtonOptions={{
type: 'error',
variant: 'error',
['data-testid' as string]: 'confirm-sign-out-button',
}}
contentOptions={{
@@ -26,7 +26,7 @@ export const StarAFFiNEModal = () => {
cancelText={t['com.affine.star-affine.cancel']()}
to={runtimeConfig.githubUrl}
confirmButtonOptions={{
type: 'primary',
variant: 'primary',
}}
confirmText={t['com.affine.star-affine.confirm']()}
external
@@ -48,7 +48,7 @@ const UpgradeSuccessLayout = ({
return (
<AuthPageContainer title={title} subtitle={subtitle}>
<Button type="primary" size="extraLarge" onClick={openAffine}>
<Button variant="primary" size="extraLarge" onClick={openAffine}>
{t['com.affine.other-page.nav.open-affine']()}
</Button>
</AuthPageContainer>
@@ -32,7 +32,7 @@ const SubscriptionChangedNotifyFooter = ({
className={clsx(actionButton, cancelButton)}
size={'default'}
onClick={onCancel}
type="plain"
variant="plain"
>
{cancelText}
</Button>
@@ -40,7 +40,7 @@ const SubscriptionChangedNotifyFooter = ({
<Button
onClick={onConfirm}
className={clsx(actionButton, confirmButton)}
type="plain"
variant="plain"
>
{okText}
</Button>
@@ -6,11 +6,7 @@ export const root = style({
height: 32,
borderRadius: 8,
boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.15)',
border: `1px solid ${cssVarV2('layer/border')}`,
borderWidth: 1,
borderColor: cssVarV2('layer/border'),
background: cssVarV2('button/siderbarPrimary/background'),
});
export const icon = style({
color: cssVarV2('icon/primary'),
fontSize: 20,
display: 'block',
});
@@ -1,4 +1,4 @@
import { Button, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
@@ -12,6 +12,7 @@ interface AddPageButtonProps {
style?: React.CSSProperties;
}
const sideBottom = { side: 'bottom' as const };
export function AddPageButton({
onClick,
className,
@@ -20,15 +21,15 @@ export function AddPageButton({
const t = useI18n();
return (
<Tooltip content={t['New Page']()} side="bottom">
<Button
data-testid="sidebar-new-page-button"
style={style}
className={clsx([styles.root, className])}
onClick={onClick}
>
<PlusIcon className={styles.icon} />
</Button>
</Tooltip>
<IconButton
tooltip={t['New Page']()}
tooltipOptions={sideBottom}
data-testid="sidebar-new-page-button"
style={style}
className={clsx([styles.root, className])}
onClick={onClick}
>
<PlusIcon />
</IconButton>
);
}
@@ -34,25 +34,20 @@ export const label = style({
lineHeight: '20px',
flexGrow: '0',
display: 'flex',
gap: 2,
alignItems: 'center',
justifyContent: 'start',
cursor: 'pointer',
});
export const collapseButton = style({
selectors: {
[`${label} > &`]: {
color: cssVarV2('icon/tertiary'),
transform: 'translateY(1px)',
},
},
});
export const collapseIcon = style({
transform: 'rotate(90deg)',
vars: { '--y': '1px', '--r': '90deg' },
color: cssVarV2('icon/tertiary'),
transform: 'translateY(var(--y)) rotate(var(--r))',
transition: 'transform 0.2s',
selectors: {
[`${root}[data-collapsed="true"] &`]: {
transform: 'rotate(0deg)',
vars: { '--r': '0deg' },
},
},
});
@@ -1,4 +1,3 @@
import { IconButton } from '@affine/component';
import { ToggleCollapseIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { type ForwardedRef, forwardRef, type PropsWithChildren } from 'react';
@@ -42,14 +41,12 @@ export const CategoryDivider = forwardRef(
<div className={styles.label}>
{label}
{collapsible ? (
<IconButton
withoutHoverStyle
className={styles.collapseButton}
size="small"
<ToggleCollapseIcon
width={16}
height={16}
data-testid="category-divider-collapse-button"
>
<ToggleCollapseIcon className={styles.collapseIcon} />
</IconButton>
className={styles.collapseIcon}
/>
) : null}
</div>
<div className={styles.actions} onClick={e => e.stopPropagation()}>
@@ -1,23 +1,18 @@
import { style } from '@vanilla-extract/css';
export const sidebarSwitch = style({
opacity: 0,
display: 'inline-flex',
export const sidebarSwitchClip = style({
flexShrink: 0,
overflow: 'hidden',
pointerEvents: 'none',
transition: 'max-width 0.2s ease-in-out, margin 0.3s ease-in-out',
transition:
'max-width 0.2s ease-in-out, margin 0.3s ease-in-out, opacity 0.3s ease',
selectors: {
'&[data-show=true]': {
maxWidth: '32px',
opacity: 1,
width: '32px',
flexShrink: 0,
fontSize: '24px',
pointerEvents: 'auto',
maxWidth: '60px',
},
'&[data-show=false]': {
opacity: 0,
maxWidth: 0,
margin: '0 !important',
},
},
});
@@ -1,7 +1,6 @@
import { IconButton, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { SidebarIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { useAtom } from 'jotai';
import { appSidebarOpenAtom } from '../index.jotai';
@@ -19,19 +18,21 @@ export const SidebarSwitch = ({
const tooltipContent = open
? t['com.affine.sidebarSwitch.collapse']()
: t['com.affine.sidebarSwitch.expand']();
// TODO(@CatsJuice): Tooltip shortcut style
const collapseKeyboardShortcuts =
environment.isBrowser && environment.isMacOs ? ' ⌘+/' : ' Ctrl+/';
return (
<Tooltip
content={tooltipContent + ' ' + collapseKeyboardShortcuts}
side={open ? 'bottom' : 'right'}
<div
data-show={show}
className={styles.sidebarSwitchClip}
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
>
<IconButton
className={clsx(styles.sidebarSwitch, className)}
data-show={show}
size="large"
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
tooltip={tooltipContent + ' ' + collapseKeyboardShortcuts}
tooltipOptions={{ side: open ? 'bottom' : 'right' }}
className={className}
size="24"
style={{
zIndex: 1,
}}
@@ -39,6 +40,6 @@ export const SidebarSwitch = ({
>
<SidebarIcon />
</IconButton>
</Tooltip>
</div>
);
};
@@ -134,7 +134,7 @@ export function patchNotificationService(
description: toReactNode(message),
confirmText,
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText,
onConfirm: () => {
@@ -177,7 +177,7 @@ export function patchNotificationService(
description: description,
confirmText: confirmText ?? 'Confirm',
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText: cancelText ?? 'Cancel',
onConfirm: () => {
@@ -1,4 +1,4 @@
import { IconButton, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { openInfoModalAtom } from '@affine/core/atoms';
import { useI18n } from '@affine/i18n';
import { InformationIcon } from '@blocksuite/icons/rc';
@@ -11,12 +11,13 @@ export const InfoButton = () => {
setOpenInfoModal(true);
};
return (
<Tooltip content={t['com.affine.page-properties.page-info.view']()}>
<IconButton
data-testid="header-info-button"
onClick={onOpenInfoModal}
icon={<InformationIcon />}
/>
</Tooltip>
<IconButton
size="20"
tooltip={t['com.affine.page-properties.page-info.view']()}
data-testid="header-info-button"
onClick={onOpenInfoModal}
>
<InformationIcon />
</IconButton>
);
};
@@ -9,9 +9,10 @@ export const DetailPageHeaderPresentButton = () => {
return (
<IconButton
style={{ flexShrink: 0 }}
size={'large'}
icon={<PresentationIcon />}
size="24"
onClick={() => handlePresent(!isPresent)}
></IconButton>
>
<PresentationIcon />
</IconButton>
);
};
@@ -11,11 +11,10 @@ export const PresentButton = () => {
return (
<Button
icon={<PresentationIcon />}
prefix={<PresentationIcon />}
className={styles.presentButton}
onClick={() => handlePresent()}
disabled={isPresent}
withoutHoverStyle
>
{t['com.affine.share-page.header.present']()}
</Button>
@@ -46,7 +46,6 @@ export const headerDivider = style({
});
export const presentButton = style({
gap: '4px',
background: cssVar('black'),
color: cssVar('white'),
borderColor: cssVar('pureBlack10'),
@@ -1,7 +1,8 @@
import type { IconButtonProps } from '@affine/component';
import { IconButton, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import Lottie from 'lottie-react';
import { forwardRef, useCallback, useState } from 'react';
@@ -25,24 +26,32 @@ export const FavoriteTag = forwardRef<
[active, onClick]
);
return (
<Tooltip content={active ? t['Favorited']() : t['Favorite']()} side="top">
<IconButton ref={ref} active={active} onClick={handleClick} {...props}>
{active ? (
playAnimation ? (
<Lottie
loop={false}
animationData={favoritedAnimation}
onComplete={() => setPlayAnimation(false)}
style={{ width: '20px', height: '20px' }}
/>
) : (
<FavoritedIcon data-testid="favorited-icon" />
)
<IconButton
tooltip={active ? t['Favorited']() : t['Favorite']()}
tooltipOptions={{ side: 'top' }}
ref={ref}
onClick={handleClick}
size="20"
{...props}
>
{active ? (
playAnimation ? (
<Lottie
loop={false}
animationData={favoritedAnimation}
onComplete={() => setPlayAnimation(false)}
style={{ width: '20px', height: '20px' }}
/>
) : (
<FavoriteIcon />
)}
</IconButton>
</Tooltip>
<FavoritedIcon
color={cssVar('primaryColor')}
data-testid="favorited-icon"
/>
)
) : (
<FavoriteIcon />
)}
</IconButton>
);
});
FavoriteTag.displayName = 'FavoriteTag';

Some files were not shown because too many files have changed in this diff Show More