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

View File

@@ -1,6 +1,7 @@
import { Button, IconButton } from '@affine/component/ui/button'; import { Button, IconButton } from '@affine/component/ui/button';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons/rc'; import { CloseIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import { useCallback } from 'react'; import { useCallback } from 'react';
import * as styles from './index.css'; import * as styles from './index.css';
@@ -37,11 +38,12 @@ export const LocalDemoTips = ({
</div> </div>
<div className={styles.tipsRightItem}> <div className={styles.tipsRightItem}>
<div> <Button style={{ background: cssVar('white') }} onClick={handleClick}>
<Button onClick={handleClick}>{buttonLabel}</Button> {buttonLabel}
</div> </Button>
<IconButton <IconButton
onClick={onClose} onClick={onClose}
size="20"
data-testid="local-demo-tips-close-button" data-testid="local-demo-tips-close-button"
> >
<CloseIcon /> <CloseIcon />

View File

@@ -41,7 +41,7 @@ export const MobileNavbar = () => {
onOpenChange: setOpenMenu, onOpenChange: setOpenMenu,
}} }}
> >
<IconButton type="plain" className={styles.iconButton}> <IconButton variant="plain" size="24" className={styles.iconButton}>
{openMenu ? <CloseIcon /> : <PropertyIcon />} {openMenu ? <CloseIcon /> : <PropertyIcon />}
</IconButton> </IconButton>
</Menu> </Menu>

View File

@@ -1,5 +1,6 @@
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc'; import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import type { FC } from 'react'; import type { FC } from 'react';
import type { ButtonProps } from '../../ui/button'; import type { ButtonProps } from '../../ui/button';
@@ -9,15 +10,15 @@ export const BackButton: FC<ButtonProps> = props => {
const t = useI18n(); const t = useI18n();
return ( return (
<Button <Button
type="plain" variant="plain"
style={{ style={{
marginTop: 12, marginTop: 12,
marginLeft: -5, marginLeft: -5,
paddingLeft: 0, paddingLeft: 0,
paddingRight: 5, paddingRight: 5,
color: 'var(--affine-text-secondary-color)', color: cssVar('textSecondaryColor'),
}} }}
icon={<ArrowLeftSmallIcon />} prefix={<ArrowLeftSmallIcon />}
{...props} {...props}
> >
{t['com.affine.backButton']()} {t['com.affine.backButton']()}

View File

@@ -58,7 +58,7 @@ export const ChangeEmailPage = ({
disabled={hasSetUp} disabled={hasSetUp}
/> />
<Button <Button
type="primary" variant="primary"
size="large" size="large"
onClick={onContinue} onClick={onContinue}
loading={loading} loading={loading}

View File

@@ -59,7 +59,7 @@ export const ChangePasswordPage: FC<{
} }
> >
{hasSetUp ? ( {hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}> <Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()} {t['com.affine.auth.open.affine']()}
</Button> </Button>
) : ( ) : (

View File

@@ -14,7 +14,7 @@ export const ConfirmChangeEmail: FC<{
title={t['com.affine.auth.change.email.page.success.title']()} title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()} 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']()} {t['com.affine.auth.open.affine']()}
</Button> </Button>
</AuthPageContainer> </AuthPageContainer>

View File

@@ -14,7 +14,7 @@ export const ConfirmChangeEmail: FC<{
title={t['com.affine.auth.change.email.page.success.title']()} title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()} 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']()} {t['com.affine.auth.open.affine']()}
</Button> </Button>
</AuthPageContainer> </AuthPageContainer>

View File

@@ -219,7 +219,7 @@ export const OnboardingPage = ({
</Button> </Button>
<Button <Button
className={styles.button} className={styles.button}
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
itemType="submit" itemType="submit"
onClick={() => { onClick={() => {
@@ -248,8 +248,7 @@ export const OnboardingPage = ({
setQuestionIdx(questionIdx + 1); setQuestionIdx(questionIdx + 1);
} }
}} }}
iconPosition="end" suffix={<ArrowRightSmallIcon />}
icon={<ArrowRightSmallIcon />}
> >
{questionIdx === 0 ? 'start' : 'Next'} {questionIdx === 0 ? 'start' : 'Next'}
</Button> </Button>
@@ -271,7 +270,7 @@ export const OnboardingPage = ({
</p> </p>
<Button <Button
className={clsx(styles.button, styles.openAFFiNEButton)} className={clsx(styles.button, styles.openAFFiNEButton)}
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
onClick={() => { onClick={() => {
if (callbackUrl) { if (callbackUrl) {
@@ -280,8 +279,7 @@ export const OnboardingPage = ({
onOpenAffine(); onOpenAffine();
} }
}} }}
iconPosition="end" suffix={<ArrowRightSmallIcon />}
icon={<ArrowRightSmallIcon />}
> >
Get Started Get Started
</Button> </Button>

View File

@@ -59,7 +59,7 @@ export const SetPasswordPage: FC<{
} }
> >
{hasSetUp ? ( {hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}> <Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()} {t['com.affine.auth.open.affine']()}
</Button> </Button>
) : ( ) : (

View File

@@ -33,7 +33,7 @@ export const SetPassword: FC<{
/> />
</Wrapper> </Wrapper>
<Button <Button
type="primary" variant="primary"
size="large" size="large"
disabled={!passwordPass} disabled={!passwordPass}
style={{ marginRight: 20 }} style={{ marginRight: 20 }}
@@ -44,7 +44,7 @@ export const SetPassword: FC<{
{t['com.affine.auth.set.password.save']()} {t['com.affine.auth.set.password.save']()}
</Button> </Button>
{showLater ? ( {showLater ? (
<Button type="plain" size="large" onClick={onLater}> <Button variant="plain" size="large" onClick={onLater}>
{t['com.affine.auth.later']()} {t['com.affine.auth.later']()}
</Button> </Button>
) : null} ) : null}

View File

@@ -13,7 +13,7 @@ export const SignInSuccessPage: FC<{
title={t['com.affine.auth.signed.success.title']()} title={t['com.affine.auth.signed.success.title']()}
subtitle={t['com.affine.auth.signed.success.subtitle']()} 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']()} {t['com.affine.auth.open.affine']()}
</Button> </Button>
</AuthPageContainer> </AuthPageContainer>

View File

@@ -63,7 +63,7 @@ export const SignUpPage: FC<{
} }
> >
{hasSetUp ? ( {hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}> <Button variant="primary" size="large" onClick={onOpenAffine}>
{openButtonText ?? t['com.affine.auth.open.affine']()} {openButtonText ?? t['com.affine.auth.open.affine']()}
</Button> </Button>
) : ( ) : (

View File

@@ -3,7 +3,6 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons/rc'; import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons/rc';
import type { WorkspaceMetadata } from '@toeverything/infra'; import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx';
import { type MouseEvent, useCallback } from 'react'; import { type MouseEvent, useCallback } from 'react';
import { Button } from '../../../ui/button'; import { Button } from '../../../ui/button';
@@ -89,8 +88,7 @@ export const WorkspaceCard = ({
<Button <Button
loading={!!openingId && openingId === meta.id} loading={!!openingId && openingId === meta.id}
disabled={!!openingId} disabled={!!openingId}
type="default" className={styles.showOnCardHover}
className={clsx(styles.enableCloudButton, styles.showOnCardHover)}
onClick={onEnableCloud} onClick={onEnableCloud}
> >
{enableCloudText} {enableCloudText}

View File

@@ -79,21 +79,16 @@ export const settingButton = style({
boxShadow: cssVar('shadow1'), boxShadow: cssVar('shadow1'),
background: cssVar('white80'), background: cssVar('white80'),
}, },
// [`.${card}:hover &:hover`]: {
// background: cssVar('hoverColor'),
// },
}, },
}); });
export const enableCloudButton = style({
background: 'transparent',
});
export const showOnCardHover = style({ export const showOnCardHover = style({
display: 'none', visibility: 'hidden',
opacity: 0,
selectors: { selectors: {
[`.${card}:hover &`]: { [`.${card}:hover &`]: {
display: 'block', visibility: 'visible',
opacity: 1,
}, },
}, },
}); });

View File

@@ -13,7 +13,7 @@ export const PublicLinkDisableModal = (props: ConfirmModalProps) => {
cancelText={t['com.affine.publicLinkDisableModal.button.cancel']()} cancelText={t['com.affine.publicLinkDisableModal.button.cancel']()}
confirmText={t['com.affine.publicLinkDisableModal.button.disable']()} confirmText={t['com.affine.publicLinkDisableModal.button.disable']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'error', variant: 'error',
['data-testid' as string]: 'confirm-enable-affine-cloud-button', ['data-testid' as string]: 'confirm-enable-affine-cloud-button',
}} }}
{...props} {...props}

View File

@@ -16,6 +16,9 @@ import {
importPageContainerStyle, importPageContainerStyle,
} from './index.css'; } from './index.css';
/**
* @deprecated Not used
*/
export const ImportPage = ({ export const ImportPage = ({
importMarkdown, importMarkdown,
importHtml, importHtml,
@@ -37,8 +40,9 @@ export const ImportPage = ({
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}
icon={<CloseIcon />} >
/> <CloseIcon />
</IconButton>
<div className={importPageBodyStyle}> <div className={importPageBodyStyle}>
<div className="title">Import</div> <div className="title">Import</div>
<span> <span>

View File

@@ -37,7 +37,7 @@ export const AcceptInvitePage = ({
</FlexWrapper> </FlexWrapper>
} }
> >
<Button type="primary" size="large" onClick={onOpenWorkspace}> <Button variant="primary" size="large" onClick={onOpenWorkspace}>
{t['Visit Workspace']()} {t['Visit Workspace']()}
</Button> </Button>
</AuthPageContainer> </AuthPageContainer>

View File

@@ -60,7 +60,7 @@ export const InviteModal = ({
confirmText={t['Invite']()} confirmText={t['Invite']()}
confirmButtonOptions={{ confirmButtonOptions={{
loading: isMutating, loading: isMutating,
type: 'primary', variant: 'primary',
['data-testid' as string]: 'confirm-enable-affine-cloud-button', ['data-testid' as string]: 'confirm-enable-affine-cloud-button',
}} }}
onConfirm={handleConfirm} onConfirm={handleConfirm}

View File

@@ -44,7 +44,7 @@ export const MemberLimitModal = ({
: 'com.affine.payment.member-limit.pro.confirm' : 'com.affine.payment.member-limit.pro.confirm'
]()} ]()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
onConfirm={handleConfirm} onConfirm={handleConfirm}
></ConfirmModal> ></ConfirmModal>

View File

@@ -3,7 +3,6 @@ import { SignOutIcon } from '@blocksuite/icons/rc';
import { Avatar } from '../../ui/avatar'; import { Avatar } from '../../ui/avatar';
import { Button, IconButton } from '../../ui/button'; import { Button, IconButton } from '../../ui/button';
import { Tooltip } from '../../ui/tooltip';
import { AffineOtherPageLayout } from '../affine-other-page-layout'; import { AffineOtherPageLayout } from '../affine-other-page-layout';
import type { User } from '../auth-components'; import type { User } from '../auth-components';
import { NotFoundPattern } from './not-found-pattern'; import { NotFoundPattern } from './not-found-pattern';
@@ -38,7 +37,7 @@ export const NoPermissionOrNotFound = ({
<p className={wrapper}>{t['404.hint']()}</p> <p className={wrapper}>{t['404.hint']()}</p>
<div className={wrapper}> <div className={wrapper}>
<Button <Button
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
onClick={onBack} onClick={onBack}
className={largeButtonEffect} className={largeButtonEffect}
@@ -49,11 +48,13 @@ export const NoPermissionOrNotFound = ({
<div className={wrapper}> <div className={wrapper}>
<Avatar url={user.avatar ?? user.image} name={user.label} /> <Avatar url={user.avatar ?? user.image} name={user.label} />
<span style={{ margin: '0 12px' }}>{user.email}</span> <span style={{ margin: '0 12px' }}>{user.email}</span>
<Tooltip content={t['404.signOut']()}> <IconButton
<IconButton onClick={onSignOut}> onClick={onSignOut}
<SignOutIcon /> size="20"
</IconButton> tooltip={t['404.signOut']()}
</Tooltip> >
<SignOutIcon />
</IconButton>
</div> </div>
</> </>
) : ( ) : (
@@ -80,7 +81,7 @@ export const NotFoundPage = ({
<p className={wrapper}>{t['404.hint']()}</p> <p className={wrapper}>{t['404.hint']()}</p>
<div className={wrapper}> <div className={wrapper}>
<Button <Button
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
onClick={onBack} onClick={onBack}
className={largeButtonEffect} className={largeButtonEffect}
@@ -93,11 +94,13 @@ export const NotFoundPage = ({
<div className={wrapper}> <div className={wrapper}>
<Avatar url={user.avatar ?? user.image} name={user.label} /> <Avatar url={user.avatar ?? user.image} name={user.label} />
<span style={{ margin: '0 12px' }}>{user.email}</span> <span style={{ margin: '0 12px' }}>{user.email}</span>
<Tooltip content={t['404.signOut']()}> <IconButton
<IconButton onClick={onSignOut}> onClick={onSignOut}
<SignOutIcon /> size="20"
</IconButton> tooltip={t['404.signOut']()}
</Tooltip> >
<SignOutIcon />
</IconButton>
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -199,21 +199,20 @@ export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(
</Tooltip> </Tooltip>
{onRemove ? ( {onRemove ? (
<Tooltip <IconButton
portalOptions={{ container: removeButtonDom }} tooltipOptions={{
{...removeTooltipOptions} portalOptions: { container: removeButtonDom },
...removeTooltipOptions,
}}
variant="solid"
size="12"
className={clsx(style.removeButton, removeButtonClassName)}
onClick={onRemove}
ref={setRemoveButtonDom}
{...removeButtonProps}
> >
<IconButton <CloseIcon />
size="extraSmall" </IconButton>
type="default"
className={clsx(style.removeButton, removeButtonClassName)}
onClick={onRemove}
ref={setRemoveButtonDom}
{...removeButtonProps}
>
<CloseIcon />
</IconButton>
</Tooltip>
) : null} ) : null}
</AvatarRoot> </AvatarRoot>
); );

View File

@@ -1,5 +1,5 @@
import { cssVar } from '@toeverything/theme'; 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 sizeVar = createVar('sizeVar');
export const blurVar = createVar('blurVar'); export const blurVar = createVar('blurVar');
const bottomAnimation = keyframes({ const bottomAnimation = keyframes({
@@ -172,7 +172,7 @@ export const hoverWrapper = style({
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(60, 61, 63, 0.5)', backgroundColor: 'rgba(60, 61, 63, 0.5)',
zIndex: '1', zIndex: '1',
color: cssVar('white'), color: cssVar('pureWhite'),
opacity: 0, opacity: 0,
transition: 'opacity .15s', transition: 'opacity .15s',
cursor: 'pointer', cursor: 'pointer',
@@ -189,14 +189,8 @@ export const removeButton = style({
visibility: 'hidden', visibility: 'hidden',
zIndex: '1', zIndex: '1',
selectors: { selectors: {
'&:hover': { [`${avatarRoot}:hover &`]: {
background: '#f6f6f6', visibility: 'visible',
}, },
}, },
}); });
globalStyle(`${avatarRoot}:hover ${removeButton}`, {
visibility: 'visible',
});
globalStyle(`${avatarRoot} ${removeButton}:hover`, {
background: '#f6f6f6',
});

View File

@@ -1,371 +1,259 @@
import { cssVar } from '@toeverything/theme'; 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({ export const button = style({
display: 'inline-flex', vars: {
justifyContent: 'center', // default vars
alignItems: 'center', [gapVar]: '4px',
userSelect: 'none', [wVar]: 'unset',
touchAction: 'manipulation', [hVar]: 'unset',
[borderWidthVar]: '1px',
},
flexShrink: 0, flexShrink: 0,
outline: '0', position: 'relative',
border: '1px solid', display: 'inline-flex',
padding: '0 8px', alignItems: 'center',
borderRadius: '8px', justifyContent: 'center',
fontSize: cssVar('fontXs'), userSelect: 'none',
fontWeight: 500, outline: 0,
borderRadius: 8,
transition: 'all .3s', transition: 'all .3s',
['WebkitAppRegion' as string]: 'no-drag',
cursor: 'pointer', cursor: 'pointer',
// changeable ['WebkitAppRegion' as string]: 'no-drag',
height: '28px',
background: cssVar('white'), // hover layer
borderColor: cssVar('borderColor'), ':before': {
color: cssVar('textPrimaryColor'), 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: { selectors: {
'&.text-bold': { '&:hover:before': { opacity: 1 },
fontWeight: 600, '&[data-block]': { display: 'flex' },
},
'&:not(.without-hover):hover': { // size
background: cssVar('hoverColor'), '&[data-size="default"]': {
}, vars: {
'&.disabled': { [hVar]: '28px', // line-height + paddingY * 2 (to ignore border width)
opacity: '.4', [paddingVar]: '0px 8px',
cursor: 'default', [iconSizeVar]: '16px',
color: cssVar('textDisableColor'), [paddingVar]: '4px 12px',
pointerEvents: 'none', [fontSizeVar]: cssVar('fontXs'),
}, [fontWeightVar]: '500',
'&.loading': { [lineHeightVar]: '20px',
cursor: 'default',
color: cssVar('textDisableColor'),
pointerEvents: 'none',
},
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
{
background: 'inherit',
}, },
'&.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%', width: '100%',
}, height: '100%',
'&.circle': { position: 'absolute',
borderRadius: '50%', top: 0,
}, left: 0,
'&.round': { borderRadius: 'inherit',
borderRadius: '14px', boxShadow: `0 0 0 1px ${cssVarV2('layer/insideBorder/primary')}`,
},
// 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'),
}, },
}, },
}); });
globalStyle(`${button} > span`, { export const content = style({
// flex: 1, // in case that width is specified by parent and text is too long
lineHeight: 1, textOverflow: 'ellipsis',
padding: '0 4px', whiteSpace: 'nowrap',
overflow: 'hidden',
}); });
export const buttonIcon = style({
export const icon = style({
flexShrink: 0, flexShrink: 0,
display: 'inline-flex', // There are two kinds of icon size:
justifyContent: 'center', // 1. control by props: width and height
alignItems: 'center', width: iconSizeVar,
color: cssVar('iconColor'), height: iconSizeVar,
fontSize: '16px', // 2. width/height is set to `1em`
width: '16px', fontSize: iconSizeVar,
height: '16px', color: iconColorVar,
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'),
},
},
}); });
globalStyle(`${icon} > svg`, {
width: '100%',
height: '100%',
display: 'block',
});
export const iconButton = style({ export const iconButton = style({
display: 'inline-flex', vars: {
justifyContent: 'center', [paddingVar]: '2px',
alignItems: 'center', // TODO(@CatsJuice): Replace with theme variables when ready
userSelect: 'none', '--shadow':
touchAction: 'manipulation', '0px 0px 1px 0px rgba(0, 0, 0, 0.12), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)',
outline: '0', },
border: '1px solid', borderRadius: 4,
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'),
selectors: { selectors: {
'&.without-padding': { '[data-theme="dark"] &': {
margin: '-2px', vars: {
}, '--shadow':
'&.active': { '0px 0px 1px 0px rgba(0, 0, 0, 0.66), 0px 1px 5px 0px rgba(0, 0, 0, 0.72)',
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',
}, },
// size
'&.large': {
width: '32px',
height: '32px',
fontSize: '24px',
}, },
'&.large.without-padding': { '&[data-icon-variant="plain"]': {
margin: '-4px', vars: {
[bgVar]: 'transparent',
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[borderWidthVar]: '0px',
},
}, },
'&.small': { '&[data-icon-variant="danger"]': {
width: '20px', vars: {
height: '20px', [bgVar]: 'transparent',
fontSize: '16px', [iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[borderWidthVar]: '0px',
},
}, },
'&.extra-small': { '&[data-icon-variant="danger"]:hover': {
width: '16px', vars: {
height: '16px', [bgVar]: cssVar('backgroundErrorColor'),
fontSize: '12px', [iconColorVar]: cssVar('errorColor'),
},
}, },
// type // disable hover layer for danger type
'&.plain': { '&[data-icon-variant="danger"]:hover:before': {
color: cssVar('iconColor'), opacity: 0,
borderColor: 'transparent',
background: 'transparent',
}, },
'&.plain.active': { '&[data-icon-variant="solid"]': {
color: cssVar('primaryColor'), vars: {
[bgVar]: cssVarV2('button/iconButtonSolid'),
[iconColorVar]: cssVarV2('icon/primary'),
[borderColorVar]: 'transparent',
[shadowVar]: 'var(--shadow)',
},
}, },
'&.primary': {
color: cssVar('white'), '&[data-icon-size="24"]': {
background: cssVar('primaryColor'), vars: { [paddingVar]: '4px' },
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'),
}, },
}, },
}); });

View File

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

View File

@@ -1,47 +1,183 @@
import { InformationIcon } from '@blocksuite/icons/rc'; import {
import type { Meta, StoryFn } from '@storybook/react'; 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 type { ButtonProps } from './button';
import { Button } from './button'; import { Button } from './button';
import * as styles from './button.stories.css';
export default { export default {
title: 'UI/Button', title: 'UI/Button',
component: Button, component: Button,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<ButtonProps>; } 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); const types: ButtonProps['variant'][] = [
Default.args = { 'primary',
type: 'default', 'secondary',
children: 'This is a default button', 'plain',
icon: <InformationIcon />, '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); export const Default = () => <Groups />;
Primary.args = {
type: 'primary', export const WithIcon = () => {
children: 'Content', return <Groups prefix={<FolderIcon />} suffix={<span>🚀</span>} />;
icon: <InformationIcon />,
}; };
export const Disabled: StoryFn<ButtonProps> = Template.bind(undefined); export const Loading = () => {
Disabled.args = { const [loading, setLoading] = useState(false);
disabled: true,
children: 'This is a disabled button', 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); export const OverrideViaClassName = () => {
LargeSizeButton.args = { const [overrideBg, setOverrideBg] = useState(false);
size: 'large', const [overrideTextColor, setOverrideTextColor] = useState(false);
children: 'This is a large button', 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> = export const FixedWidth = () => {
Template.bind(undefined); const widths = [60, 100, 120, 160, 180];
ExtraLargeSizeButton.args = { return (
size: 'extraLarge', <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
children: 'This is a extra large button', {widths.map(width => (
<Button prefix={<AfFiNeIcon />} key={width} style={{ width }}>
This is a width fixed button
</Button>
))}
</div>
);
};
export const Disabled = () => {
return <Groups disabled />;
}; };

View File

@@ -1,172 +1,168 @@
import clsx from 'clsx'; import clsx from 'clsx';
import type { import type {
FC, CSSProperties,
HTMLAttributes, HTMLAttributes,
PropsWithChildren, MouseEvent,
ReactElement, ReactElement,
} from 'react'; } from 'react';
import { forwardRef, useMemo } from 'react'; import { cloneElement, forwardRef, useCallback } from 'react';
import { Loading } from '../loading'; 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 = export type ButtonType =
| 'default'
| 'primary' | 'primary'
| 'secondary'
| 'plain' | 'plain'
| 'error' | 'error'
| 'warning'
| 'success' | 'success'
| 'processing'; | 'custom';
export type ButtonSize = 'default' | 'large' | 'extraLarge'; export type ButtonSize = 'default' | 'large' | 'extraLarge' | 'custom';
type BaseButtonProps = {
type?: ButtonType; export interface ButtonProps
extends Omit<HTMLAttributes<HTMLButtonElement>, 'type' | 'prefix'> {
/**
* Preset color scheme
* @default 'secondary'
*/
variant?: ButtonType;
disabled?: boolean; disabled?: boolean;
icon?: ReactElement; /**
iconPosition?: 'start' | 'end'; * By default, the button is `inline-flex`, set to `true` to make it `flex`
shape?: 'default' | 'round' | 'circle'; * @default false
*/
block?: boolean; block?: boolean;
/**
* Preset size, will be overridden by `style` or `className`
* @default 'default'
*/
size?: ButtonSize; size?: ButtonSize;
/**
* Will show a loading spinner at `prefix` position
*/
loading?: boolean; loading?: boolean;
withoutHoverStyle?: boolean;
};
export type ButtonProps = PropsWithChildren<BaseButtonProps> & /**
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & { * By default, it is considered as an icon with preset size and color,
componentProps?: { * can be overridden by `prefixClassName` and `prefixStyle`.
startIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>; *
endIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>; * 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 = { tooltip?: TooltipProps['content'];
type: 'default', tooltipOptions?: Partial<Omit<TooltipProps, 'content'>>;
disabled: false, }
shape: 'default',
size: 'default',
iconPosition: 'start',
loading: false,
withoutHoverStyle: false,
} as const;
const ButtonIcon: FC<IconButtonProps> = props => { const IconSlot = ({
const { icon,
size, loading,
icon, className,
iconPosition = 'start', ...attrs
children, }: {
type, icon?: ReactElement;
loading, loading?: boolean;
withoutHoverStyle, } & HTMLAttributes<HTMLElement>) => {
...otherProps const showLoadingHere = loading !== undefined;
} = { const visible = icon || loading;
...defaultProps, return visible ? (
...props, <div className={clsx(styles.icon, className)} {...attrs}>
}; {showLoadingHere && loading ? <Loading size="100%" /> : null}
const onlyIcon = icon && !children; {icon && !loading
return ( ? cloneElement(icon, {
<div width: '100%',
{...otherProps} height: '100%',
className={clsx(buttonIcon, { ...icon.props,
'color-white': type && type !== 'default' && type !== 'plain', })
large: size === 'large', : null}
extraLarge: size === 'extraLarge',
end: iconPosition === 'end' && !onlyIcon,
start: iconPosition === 'start' && !onlyIcon,
loading,
})}
data-without-hover={withoutHoverStyle}
>
{icon}
</div> </div>
); ) : null;
}; };
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => { (
const { {
variant = 'secondary',
size = 'default',
children, children,
type,
disabled, disabled,
shape,
size,
icon: propsIcon,
iconPosition,
block, block,
loading, loading,
withoutHoverStyle,
className, className,
prefix,
prefixClassName,
prefixStyle,
suffix,
suffixClassName,
suffixStyle,
contentClassName,
contentStyle,
tooltip,
tooltipOptions,
onClick,
...otherProps ...otherProps
} = { },
...defaultProps, ref
...props, ) => {
} satisfies ButtonProps; const handleClick = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
const icon = useMemo(() => { if (loading || disabled) return;
if (loading) { onClick?.(e);
return <Loading />; },
} [disabled, loading, onClick]
return propsIcon; );
}, [propsIcon, loading]);
const baseIconButtonProps = useMemo(() => {
return {
size,
iconPosition,
icon,
type,
disabled,
loading,
} as const;
}, [disabled, icon, iconPosition, loading, size, type]);
return ( return (
<button <Tooltip content={tooltip} {...tooltipOptions}>
{...otherProps} <button
ref={ref} {...otherProps}
className={clsx( ref={ref}
button, className={clsx(styles.button, className)}
{ data-loading={loading || undefined}
primary: type === 'primary', data-block={block || undefined}
plain: type === 'plain', disabled={disabled}
error: type === 'error', data-disabled={disabled || undefined}
warning: type === 'warning', data-size={size}
success: type === 'success', data-variant={variant}
processing: type === 'processing', onClick={handleClick}
large: size === 'large', >
extraLarge: size === 'extraLarge', <IconSlot
disabled, icon={prefix}
circle: shape === 'circle', loading={loading}
round: shape === 'round', className={prefixClassName}
block, style={prefixStyle}
loading,
'without-hover': withoutHoverStyle,
},
className
)}
disabled={disabled}
data-disabled={disabled}
>
{icon && iconPosition === 'start' ? (
<ButtonIcon
{...baseIconButtonProps}
{...props.componentProps?.startIcon}
icon={icon}
iconPosition="start"
/> />
) : null} {children ? (
<span>{children}</span> <span
{icon && iconPosition === 'end' ? ( className={clsx(styles.content, contentClassName)}
<ButtonIcon style={contentStyle}
{...baseIconButtonProps} >
{...props.componentProps?.endIcon} {children}
icon={icon} </span>
iconPosition="end" ) : null}
<IconSlot
icon={suffix}
className={suffixClassName}
style={suffixStyle}
/> />
) : null} </button>
</button> </Tooltip>
); );
} }
); );

View File

@@ -1,49 +1,153 @@
import { InformationIcon } from '@blocksuite/icons/rc'; import { AfFiNeIcon } from '@blocksuite/icons/rc';
import type { Meta, StoryFn } from '@storybook/react'; 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 type { IconButtonProps } from './icon-button';
import { IconButton } from './icon-button'; import { IconButton } from './icon-button';
export default { export default {
title: 'UI/IconButton', title: 'UI/IconButton',
component: IconButton, component: IconButton,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<IconButtonProps>; } 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); const Groups = ({
Plain.args = { children,
children: <InformationIcon />, ...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); export const Default = () => <Groups />;
Primary.args = {
type: 'primary', export const Loading = () => {
icon: <InformationIcon />, 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); export const OverrideViaClassName = () => {
Disabled.args = { const [overrideBg, setOverrideBg] = useState(false);
disabled: true, const [overrideBorder, setOverrideBorder] = useState(false);
icon: <InformationIcon />, 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); export const CustomSize = () => {
ExtraSmallSizeButton.args = { const sizes = [
size: 'extraSmall', [13, 2],
icon: <InformationIcon />, [15, 2],
}; [17, 2],
export const SmallSizeButton: StoryFn<IconButtonProps> = [19, 2],
Template.bind(undefined); [21, 3],
SmallSizeButton.args = { [23, 3],
size: 'small', [25, 3],
icon: <InformationIcon />, [27, 3],
}; [29, 4],
export const LargeSizeButton: StoryFn<IconButtonProps> = [31, 4],
Template.bind(undefined); [33, 4],
LargeSizeButton.args = { [35, 4],
size: 'large', ];
icon: <InformationIcon />, 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 />;

View File

@@ -1,85 +1,78 @@
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; import { type CSSProperties, forwardRef, type ReactElement } from 'react';
import { forwardRef } from 'react';
import { Loading } from '../loading'; import { Button, type ButtonProps } from './button';
import type { ButtonType } from './button'; import { iconButton, iconSizeVar } from './button.css';
import { iconButton } from './button.css';
export type IconButtonSize = 'default' | 'large' | 'small' | 'extraSmall'; export interface IconButtonProps
export type IconButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & extends Omit<
PropsWithChildren<{ ButtonProps,
type?: ButtonType; | 'variant'
disabled?: boolean; | 'size'
size?: IconButtonSize; | 'prefix'
loading?: boolean; | 'suffix'
withoutPadding?: boolean; | 'children'
active?: boolean; | 'prefixClassName'
withoutHoverStyle?: boolean; | 'prefixStyle'
icon?: ReactElement; | 'suffix'
}>; | 'suffixClassName'
| 'suffixStyle'
const defaultProps = { > {
type: 'plain', /** Icon element */
disabled: false, children?: ReactElement;
size: 'default', /** Same as `children`, compatibility of the old API */
loading: false, icon?: ReactElement;
withoutPadding: false, variant?: 'plain' | 'solid' | 'danger' | 'custom';
active: false, /**
withoutHoverStyle: false, * Use preset size,
} as const; * 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>( export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(props, ref) => { (
const { {
type, variant = 'plain',
size, size = '20',
withoutPadding, style,
children,
disabled,
loading,
active,
withoutHoverStyle,
icon: propsIcon,
className, className,
children,
icon,
iconClassName,
iconStyle,
...otherProps ...otherProps
} = { },
...defaultProps, ref
...props, ) => {
}; const validatedSize = isNaN(parseInt(size as string, 10)) ? 16 : size;
return ( return (
<button <Button
ref={ref} ref={ref}
className={clsx( style={{
iconButton, ...style,
{ ...assignInlineVars({
'without-padding': withoutPadding, [iconSizeVar]: `${validatedSize}px`,
}),
primary: type === 'primary', }}
plain: type === 'plain', data-icon-variant={variant}
error: type === 'error', data-icon-size={validatedSize}
warning: type === 'warning', className={clsx(iconButton, className)}
success: type === 'success', size={'custom'}
processing: type === 'processing', variant={'custom'}
prefix={children ?? icon}
large: size === 'large', prefixClassName={iconClassName}
small: size === 'small', prefixStyle={iconStyle}
'extra-small': size === 'extraSmall',
disabled,
loading,
active,
'without-hover': withoutHoverStyle,
},
className
)}
disabled={disabled}
data-disabled={disabled}
{...otherProps} {...otherProps}
> />
{loading ? <Loading /> : children || propsIcon}
</button>
); );
} }
); );

View File

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

View File

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

View File

@@ -134,8 +134,7 @@ export const NavButtons = memo(function NavButtons({
<div className={styles.headerNavButtons} key="nav-btn-group"> <div className={styles.headerNavButtons} key="nav-btn-group">
<IconButton <IconButton
key="nav-btn-prev" key="nav-btn-prev"
size="small" size="16"
className={styles.focusInteractive}
disabled={prevDisabled} disabled={prevDisabled}
data-testid="date-picker-nav-prev" data-testid="date-picker-nav-prev"
onClick={onPrev} onClick={onPrev}
@@ -147,8 +146,7 @@ export const NavButtons = memo(function NavButtons({
<IconButton <IconButton
key="nav-btn-next" key="nav-btn-next"
size="small" size="16"
className={styles.focusInteractive}
disabled={nextDisabled} disabled={nextDisabled}
data-testid="date-picker-nav-next" data-testid="date-picker-nav-next"
onClick={onNext} onClick={onNext}

View File

@@ -1,9 +1,10 @@
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import { withUnit } from '../../utils/with-unit';
import { loading, speedVar } from './styles.css'; import { loading, speedVar } from './styles.css';
export interface LoadingProps { export interface LoadingProps {
size?: number; size?: number | string;
speed?: number; speed?: number;
progress?: number; progress?: number;
} }
@@ -13,11 +14,13 @@ export const Loading = ({
speed = 1.2, speed = 1.2,
progress = 0.2, progress = 0.2,
}: LoadingProps) => { }: LoadingProps) => {
// allow `string` such as `16px` | `100%` | `1em`
const sizeWithUnit = size ? withUnit(size, 'px') : '16px';
return ( return (
<svg <svg
className={loading} className={loading}
width={size ? `${size}px` : '16px'} width={sizeWithUnit}
height={size ? `${size}px` : '16px'} height={sizeWithUnit}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -61,7 +61,7 @@ const ConfirmModalTemplate: StoryFn<ConfirmModalProps> = () => {
confirmText="Confirm" confirmText="Confirm"
confirmButtonOptions={{ confirmButtonOptions={{
loading: loading, loading: loading,
type: 'primary', variant: 'primary',
}} }}
> >
<Input placeholder="input someting" status={inputStatus} /> <Input placeholder="input someting" status={inputStatus} />
@@ -82,7 +82,7 @@ const OverlayModalTemplate: StoryFn<OverlayModalProps> = () => {
title="Modal Title" title="Modal Title"
description="Modal description" description="Modal description"
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
topImage={ topImage={
<div <div

View File

@@ -130,10 +130,12 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
style: overlayStyle, style: overlayStyle,
...otherOverlayOptions ...otherOverlayOptions
} = {}, } = {},
closeButtonOptions = {}, closeButtonOptions,
children, children,
...otherProps ...otherProps
} = props; } = props;
const { className: closeButtonClassName, ...otherCloseButtonProps } =
closeButtonOptions || {};
const [container, setContainer] = useState<ModalTransitionContainer | null>( const [container, setContainer] = useState<ModalTransitionContainer | null>(
null null
@@ -204,11 +206,11 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
{withoutCloseButton ? null : ( {withoutCloseButton ? null : (
<Dialog.Close asChild> <Dialog.Close asChild>
<IconButton <IconButton
className={styles.closeButton} size="20"
className={clsx(styles.closeButton, closeButtonClassName)}
aria-label="Close" aria-label="Close"
type="plain"
data-testid="modal-close-button" data-testid="modal-close-button"
{...closeButtonOptions} {...otherCloseButtonProps}
> >
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>

View File

@@ -32,7 +32,7 @@ export const AdminPanelHeader = ({
</div> </div>
<div> <div>
<Button <Button
type="primary" variant="primary"
disabled={modifiedValues.length === 0} disabled={modifiedValues.length === 0}
onClick={() => { onClick={() => {
openConfirmModal({ openConfirmModal({
@@ -41,7 +41,7 @@ export const AdminPanelHeader = ({
'Are you sure you want to save the following changes?', 'Are you sure you want to save the following changes?',
confirmText: 'Save', confirmText: 'Save',
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
}, },
onConfirm: onConfirm, onConfirm: onConfirm,
children: children:

View File

@@ -81,7 +81,7 @@ export const ErrorDetail: FC<ErrorDetailProps> = props => {
))} ))}
<div className={styles.errorFooter}> <div className={styles.errorFooter}>
<Button <Button
type="primary" variant="primary"
onClick={onBtnClick} onClick={onBtnClick}
loading={isBtnLoading} loading={isBtnLoading}
size="extraLarge" size="extraLarge"

View File

@@ -21,7 +21,6 @@ export const thumbContent = style({
}); });
export const actionButton = style({ export const actionButton = style({
fontWeight: 500,
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
lineHeight: '22px', lineHeight: '22px',
}); });

View File

@@ -103,7 +103,7 @@ export const AIOnboardingEdgeless = () => {
notify.dismiss(id); notify.dismiss(id);
toggleEdgelessAIOnboarding(false); toggleEdgelessAIOnboarding(false);
}} }}
type="plain" variant="plain"
className={styles.actionButton} className={styles.actionButton}
> >
<span className={styles.getStartedButtonText}> <span className={styles.getStartedButtonText}>
@@ -113,7 +113,7 @@ export const AIOnboardingEdgeless = () => {
{aiSubscription ? null : ( {aiSubscription ? null : (
<Button <Button
className={styles.actionButton} className={styles.actionButton}
type="plain" variant="plain"
onClick={() => { onClick={() => {
goToPricingPlans(); goToPricingPlans();
notify.dismiss(id); notify.dismiss(id);

View File

@@ -114,17 +114,3 @@ export const subscribeActions = style({
gap: 12, gap: 12,
alignItems: 'center', alignItems: 'center',
}); });
export const baseActionButton = style({
fontSize: cssVar('fontBase'),
selectors: {
'&.large': {
fontWeight: 500,
},
},
});
export const transparentActionButton = style([
baseActionButton,
{
backgroundColor: 'transparent',
},
]);

View File

@@ -243,36 +243,26 @@ export const AIOnboardingGeneral = () => {
> >
{isLast ? ( {isLast ? (
<> <>
<IconButton <IconButton size="20" onClick={onPrev}>
size="default" <ArrowLeftSmallIcon />
icon={<ArrowLeftSmallIcon width={20} height={20} />} </IconButton>
onClick={onPrev}
type="plain"
className={styles.baseActionButton}
/>
{aiSubscription ? ( {aiSubscription ? (
<Button <Button
className={styles.baseActionButton}
size="large" size="large"
onClick={closeAndDismiss} onClick={closeAndDismiss}
type="primary" variant="primary"
> >
{t['com.affine.ai-onboarding.general.get-started']()} {t['com.affine.ai-onboarding.general.get-started']()}
</Button> </Button>
) : ( ) : (
<div className={styles.subscribeActions}> <div className={styles.subscribeActions}>
<Button <Button size="large" onClick={goToPricingPlans}>
className={styles.baseActionButton}
size="large"
onClick={goToPricingPlans}
>
{t['com.affine.ai-onboarding.general.purchase']()} {t['com.affine.ai-onboarding.general.purchase']()}
</Button> </Button>
<Button <Button
className={styles.baseActionButton}
size="large" size="large"
onClick={closeAndDismiss} onClick={closeAndDismiss}
type="primary" variant="primary"
> >
{t['com.affine.ai-onboarding.general.try-for-free']()} {t['com.affine.ai-onboarding.general.try-for-free']()}
</Button> </Button>
@@ -282,21 +272,15 @@ export const AIOnboardingGeneral = () => {
) : ( ) : (
<> <>
{isFirst ? ( {isFirst ? (
<Button <Button onClick={remindLater} size="large">
className={styles.transparentActionButton}
onClick={remindLater}
size="large"
type="default"
>
{t['com.affine.ai-onboarding.general.skip']()} {t['com.affine.ai-onboarding.general.skip']()}
</Button> </Button>
) : ( ) : (
<Button <Button
icon={<ArrowLeftSmallIcon />} prefix={<ArrowLeftSmallIcon />}
className={styles.baseActionButton}
onClick={onPrev} onClick={onPrev}
type="plain"
size="large" size="large"
variant="plain"
> >
{t['com.affine.ai-onboarding.general.prev']()} {t['com.affine.ai-onboarding.general.prev']()}
</Button> </Button>
@@ -305,12 +289,7 @@ export const AIOnboardingGeneral = () => {
<div> <div>
{index + 1} / {list.length} {index + 1} / {list.length}
</div> </div>
<Button <Button size="large" variant="primary" onClick={onNext}>
className={styles.baseActionButton}
size="large"
type="primary"
onClick={onNext}
>
{t['com.affine.ai-onboarding.general.next']()} {t['com.affine.ai-onboarding.general.next']()}
</Button> </Button>
</div> </div>

View File

@@ -41,7 +41,7 @@ const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer"> <a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
<Button <Button
className={styles.actionButton} className={styles.actionButton}
type="plain" variant="plain"
onClick={onDismiss} onClick={onDismiss}
> >
{t['com.affine.ai-onboarding.local.action-learn-more']()} {t['com.affine.ai-onboarding.local.action-learn-more']()}
@@ -50,7 +50,7 @@ const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
{loggedIn ? null : ( {loggedIn ? null : (
<Button <Button
className={styles.actionButton} className={styles.actionButton}
type="plain" variant="plain"
onClick={() => { onClick={() => {
onDismiss(); onDismiss();
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' }); jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });

View File

@@ -99,7 +99,7 @@ export const AfterSignInSendEmail = ({
<Button <Button
style={!verifyToken ? { cursor: 'not-allowed' } : {}} style={!verifyToken ? { cursor: 'not-allowed' } : {}}
disabled={!verifyToken || isSending} disabled={!verifyToken || isSending}
type="plain" variant="plain"
size="large" size="large"
onClick={onResendClick} onClick={onResendClick}
> >

View File

@@ -91,7 +91,7 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
<Button <Button
style={!verifyToken ? { cursor: 'not-allowed' } : {}} style={!verifyToken ? { cursor: 'not-allowed' } : {}}
disabled={!verifyToken || isSending} disabled={!verifyToken || isSending}
type="plain" variant="plain"
size="large" size="large"
onClick={onResendClick} onClick={onResendClick}
> >

View File

@@ -27,7 +27,7 @@ export const AiLoginRequiredModal = () => {
}, },
confirmText: t['com.affine.ai.login-required.dialog-confirm'](), confirmText: t['com.affine.ai.login-required.dialog-confirm'](),
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
}, },
cancelText: t['com.affine.ai.login-required.dialog-cancel'](), cancelText: t['com.affine.ai.login-required.dialog-cancel'](),
onOpenChange: setOpen, onOpenChange: setOpen,

View File

@@ -82,11 +82,11 @@ function OAuthProvider({
return ( return (
<Button <Button
key={provider} key={provider}
type="primary" variant="primary"
block block
size="extraLarge" size="extraLarge"
style={{ marginTop: 30 }} style={{ marginTop: 30, width: '100%' }}
icon={icon} prefix={icon}
onClick={onClick} onClick={onClick}
> >
Continue with {provider} Continue with {provider}

View File

@@ -203,7 +203,7 @@ export const SendEmail = ({
</Wrapper> </Wrapper>
<Button <Button
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
style={{ width: '100%' }} style={{ width: '100%' }}
disabled={hasSentEmail} disabled={hasSentEmail}

View File

@@ -129,7 +129,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
</div> </div>
<Button <Button
data-testid="sign-in-button" data-testid="sign-in-button"
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
style={{ width: '100%' }} style={{ width: '100%' }}
disabled={isLoading} disabled={isLoading}

View File

@@ -5,8 +5,9 @@ import { authAtom } from '@affine/core/atoms';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel'; import { mixpanel } from '@affine/core/mixpanel';
import { Trans, useI18n } from '@affine/i18n'; 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 { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -149,21 +150,13 @@ export const SignIn: FC<AuthPanelProps> = ({
{verifyToken ? ( {verifyToken ? (
<Button <Button
style={{ width: '100%' }}
size="extraLarge" size="extraLarge"
data-testid="continue-login-button" data-testid="continue-login-button"
block block
loading={isMutating} loading={isMutating}
icon={ suffix={<ArrowRightBigIcon />}
<ArrowDownBigIcon suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
}
iconPosition="end"
onClick={onContinue} onClick={onContinue}
> >
{t['com.affine.auth.sign.email.continue']()} {t['com.affine.auth.sign.email.continue']()}

View File

@@ -106,7 +106,7 @@ const NameWorkspaceContent = ({
cancelText={t['com.affine.nameWorkspace.button.cancel']()} cancelText={t['com.affine.nameWorkspace.button.cancel']()}
confirmText={t['com.affine.nameWorkspace.button.create']()} confirmText={t['com.affine.nameWorkspace.button.create']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
disabled: !workspaceName || loading, disabled: !workspaceName || loading,
['data-testid' as string]: 'create-workspace-create-button', ['data-testid' as string]: 'create-workspace-create-button',
}} }}

View File

@@ -28,7 +28,7 @@ export const HistoryTipsModal = () => {
description={t['com.affine.history-vision.tips-modal.description']()} description={t['com.affine.history-vision.tips-modal.description']()}
cancelText={t['com.affine.history-vision.tips-modal.cancel']()} cancelText={t['com.affine.history-vision.tips-modal.cancel']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
onConfirm={handleConfirm} onConfirm={handleConfirm}
confirmText={t['com.affine.history-vision.tips-modal.confirm']()} confirmText={t['com.affine.history-vision.tips-modal.confirm']()}

View File

@@ -27,7 +27,7 @@ export const IssueFeedbackModal = () => {
to={`${runtimeConfig.githubUrl}/issues/new/choose`} to={`${runtimeConfig.githubUrl}/issues/new/choose`}
confirmText={t['com.affine.issue-feedback.confirm']()} confirmText={t['com.affine.issue-feedback.confirm']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
external external
/> />

View File

@@ -19,7 +19,7 @@ export const AnimateInTooltip = ({
</div> </div>
<div className={styles.next}> <div className={styles.next}>
{visible ? ( {visible ? (
<Button type="primary" size="extraLarge" onClick={onNext}> <Button variant="primary" size="extraLarge" onClick={onNext}>
Next Next
</Button> </Button>
) : null} ) : null}

View File

@@ -235,7 +235,7 @@ export const EdgelessSwitch = ({
onSwitchToPageMode={onSwitchToPageMode} onSwitchToPageMode={onSwitchToPageMode}
onSwitchToEdgelessMode={onSwitchToEdgelessMode} onSwitchToEdgelessMode={onSwitchToEdgelessMode}
/> />
<Button size="extraLarge" type="primary" onClick={onNextClick}> <Button size="extraLarge" variant="primary" onClick={onNextClick}>
Next Next
</Button> </Button>
</header> </header>
@@ -263,7 +263,7 @@ export const EdgelessSwitch = ({
<Button <Button
className={styles.wellDoneEnterAnim} className={styles.wellDoneEnterAnim}
onClick={onNextClick} onClick={onNextClick}
type="primary" variant="primary"
size="extraLarge" size="extraLarge"
style={{ marginTop: 40 }} style={{ marginTop: 40 }}
> >

View File

@@ -251,11 +251,9 @@ const PlanPrompt = () => {
: '' /* TODO(@catsjuice): loading UI */ : '' /* TODO(@catsjuice): loading UI */
} }
<IconButton <IconButton onClick={closeFreePlanPrompt}>
size="small" <CloseIcon />
icon={<CloseIcon />} </IconButton>
onClick={closeFreePlanPrompt}
/>
</div> </div>
); );
}, [closeFreePlanPrompt, isProWorkspace, t]); }, [closeFreePlanPrompt, isProWorkspace, t]);
@@ -393,7 +391,7 @@ const PageHistoryList = ({
})} })}
{onLoadMore ? ( {onLoadMore ? (
<Button <Button
type="plain" variant="plain"
loading={loadingMore} loading={loadingMore}
disabled={loadingMore} disabled={loadingMore}
className={styles.historyItemLoadMore} className={styles.historyItemLoadMore}
@@ -480,7 +478,7 @@ const PageHistoryManager = ({
}, },
confirmText: t['com.affine.history.confirm-restore-modal.restore'](), confirmText: t['com.affine.history.confirm-restore-modal.restore'](),
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
['data-testid' as string]: 'confirm-restore-history-button', ['data-testid' as string]: 'confirm-restore-history-button',
}, },
onConfirm: handleRestore, onConfirm: handleRestore,
@@ -520,12 +518,12 @@ const PageHistoryManager = ({
) : null} ) : null}
<div className={styles.historyFooter}> <div className={styles.historyFooter}>
<Button type="plain" onClick={onClose}> <Button onClick={onClose}>
{t['com.affine.history.back-to-page']()} {t['com.affine.history.back-to-page']()}
</Button> </Button>
<div className={styles.spacer} /> <div className={styles.spacer} />
<Button <Button
type="primary" variant="primary"
onClick={onConfirmRestore} onClick={onConfirmRestore}
disabled={isMutating || !activeVersion} disabled={isMutating || !activeVersion}
> >

View File

@@ -50,7 +50,7 @@ export const ConfirmDeletePropertyModal = ({
onClick: onCancel, onClick: onCancel,
}} }}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'error', variant: 'error',
}} }}
/> />
); );

View File

@@ -117,22 +117,14 @@ export const tableBodySortable = style({
}); });
export const addPropertyButton = style({ export const addPropertyButton = style({
display: 'flex',
alignItems: 'center',
alignSelf: 'flex-start', alignSelf: 'flex-start',
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
color: `${cssVar('textSecondaryColor')} !important`, color: `${cssVar('textSecondaryColor')}`,
padding: '0 4px', padding: '0 4px',
height: 36, height: 36,
cursor: 'pointer',
':hover': {
color: cssVar('textPrimaryColor'),
backgroundColor: cssVar('hoverColor'),
},
gap: 2,
fontWeight: 400, fontWeight: 400,
gap: 6,
}); });
globalStyle(`${addPropertyButton} svg`, { globalStyle(`${addPropertyButton} svg`, {
fontSize: 16, fontSize: 16,
color: cssVar('iconSecondary'), color: cssVar('iconSecondary'),

View File

@@ -703,11 +703,9 @@ export const PagePropertiesTableHeader = ({
</div> </div>
{properties.length === 0 || manager.readonly ? null : ( {properties.length === 0 || manager.readonly ? null : (
<PagePropertiesSettingsPopup> <PagePropertiesSettingsPopup>
<IconButton <IconButton data-testid="page-info-show-more" size="20">
data-testid="page-info-show-more" <MoreHorizontalIcon />
type="plain" </IconButton>
icon={<MoreHorizontalIcon />}
/>
</PagePropertiesSettingsPopup> </PagePropertiesSettingsPopup>
)} )}
<Collapsible.Trigger asChild role="button" onClick={handleCollapse}> <Collapsible.Trigger asChild role="button" onClick={handleCollapse}>
@@ -715,15 +713,12 @@ export const PagePropertiesTableHeader = ({
className={styles.tableHeaderCollapseButtonWrapper} className={styles.tableHeaderCollapseButtonWrapper}
data-testid="page-info-collapse" data-testid="page-info-collapse"
> >
<IconButton <IconButton size="20">
type="plain" <ToggleExpandIcon
icon={ className={styles.collapsedIcon}
<ToggleExpandIcon data-collapsed={!open}
className={styles.collapsedIcon} />
data-collapsed={!open} </IconButton>
/>
}
/>
</div> </div>
</Collapsible.Trigger> </Collapsible.Trigger>
</div> </div>
@@ -1056,8 +1051,8 @@ export const PagePropertiesAddProperty = () => {
return ( return (
<Menu {...menuOptions}> <Menu {...menuOptions}>
<Button <Button
type="plain" variant="plain"
icon={<PlusIcon />} prefix={<PlusIcon />}
className={styles.addPropertyButton} className={styles.addPropertyButton}
> >
{t['com.affine.page-properties.add-property']()} {t['com.affine.page-properties.add-property']()}

View File

@@ -405,11 +405,9 @@ export const TagsEditor = ({ pageId, readonly }: TagsEditorProps) => {
<TagItem maxWidth="100%" tag={tag} mode="inline" /> <TagItem maxWidth="100%" tag={tag} mode="inline" />
<div className={styles.spacer} /> <div className={styles.spacer} />
<EditTagMenu tagId={tag.id} onTagDelete={onTagDelete}> <EditTagMenu tagId={tag.id} onTagDelete={onTagDelete}>
<IconButton <IconButton className={styles.tagEditIcon}>
className={styles.tagEditIcon} <MoreHorizontalIcon />
type="plain" </IconButton>
icon={<MoreHorizontalIcon />}
/>
</EditTagMenu> </EditTagMenu>
</div> </div>
); );

View File

@@ -112,7 +112,7 @@ export const CloudQuotaModal = () => {
isFreePlanOwner ? t['com.affine.payment.upgrade']() : t['Got it']() isFreePlanOwner ? t['com.affine.payment.upgrade']() : t['Got it']()
} }
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
/> />
); );

View File

@@ -37,7 +37,7 @@ export const LocalQuotaModal = () => {
onConfirm={onConfirm} onConfirm={onConfirm}
confirmText={t['Got it']()} confirmText={t['Got it']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
/> />
); );

View File

@@ -132,7 +132,7 @@ export const AIUsagePanel = () => {
</div> </div>
{hasPaymentFeature && ( {hasPaymentFeature && (
<AISubscribe type="primary" className={styles.storageButton}> <AISubscribe variant="primary">
{t['com.affine.payment.ai.usage.purchase-button-label']()} {t['com.affine.payment.ai.usage.purchase-button-label']()}
</AISubscribe> </AISubscribe>
)} )}

View File

@@ -142,7 +142,6 @@ export const AvatarAndName = () => {
<Button <Button
data-testid="save-user-name" data-testid="save-user-name"
onClick={handleUpdateUserName} onClick={handleUpdateUserName}
className={styles.button}
style={{ style={{
marginLeft: '12px', marginLeft: '12px',
}} }}
@@ -234,7 +233,7 @@ export const AccountSetting: FC = () => {
/> />
<AvatarAndName /> <AvatarAndName />
<SettingRow name={t['com.affine.settings.email']()} desc={account.email}> <SettingRow name={t['com.affine.settings.email']()} desc={account.email}>
<Button onClick={onChangeEmail} className={styles.button}> <Button onClick={onChangeEmail}>
{account.info?.emailVerified {account.info?.emailVerified
? t['com.affine.settings.email.action.change']() ? t['com.affine.settings.email.action.change']()
: t['com.affine.settings.email.action.verify']()} : t['com.affine.settings.email.action.verify']()}
@@ -244,7 +243,7 @@ export const AccountSetting: FC = () => {
name={t['com.affine.settings.password']()} name={t['com.affine.settings.password']()}
desc={t['com.affine.settings.password.message']()} desc={t['com.affine.settings.password.message']()}
> >
<Button onClick={onPasswordButtonClick} className={styles.button}> <Button onClick={onPasswordButtonClick}>
{account.info?.hasPassword {account.info?.hasPassword
? t['com.affine.settings.password.action.change']() ? t['com.affine.settings.password.action.change']()
: t['com.affine.settings.password.action.set']()} : t['com.affine.settings.password.action.set']()}

View File

@@ -28,6 +28,3 @@ globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, {
export const storageProgressBar = style({ export const storageProgressBar = style({
height: '100%', height: '100%',
}); });
export const storageButton = style({
padding: '4px 12px',
});

View File

@@ -18,7 +18,7 @@ export interface StorageProgressProgress {
enum ButtonType { enum ButtonType {
Primary = 'primary', Primary = 'primary',
Default = 'default', Default = 'secondary',
} }
export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => { export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
@@ -101,11 +101,7 @@ export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
} }
> >
<span tabIndex={0}> <span tabIndex={0}>
<Button <Button variant={buttonType} onClick={onUpgrade}>
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{isFreeUser {isFreeUser
? t['com.affine.storage.upgrade']() ? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()} : t['com.affine.storage.change-plan']()}

View File

@@ -39,6 +39,3 @@ globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
color: cssVar('white'), color: cssVar('white'),
fontSize: cssVar('fontH4'), fontSize: cssVar('fontH4'),
}); });
export const button = style({
padding: '4px 12px',
});

View File

@@ -299,9 +299,7 @@ const TypeFormLink = () => {
desc={t['com.affine.payment.billing-type-form.description']()} desc={t['com.affine.payment.billing-type-form.description']()}
> >
<a target="_blank" href={link} rel="noreferrer"> <a target="_blank" href={link} rel="noreferrer">
<Button style={{ padding: '4px 12px' }}> <Button>{t['com.affine.payment.billing-type-form.go']()}</Button>
{t['com.affine.payment.billing-type-form.go']()}
</Button>
</a> </a>
</SettingRow> </SettingRow>
); );
@@ -435,7 +433,7 @@ const PlanAction = ({
return ( return (
<Button <Button
className={styles.planAction} className={styles.planAction}
type="primary" variant="primary"
onClick={gotoPlansSetting} onClick={gotoPlansSetting}
> >
{plan === SubscriptionPlan.Pro {plan === SubscriptionPlan.Pro
@@ -460,12 +458,7 @@ const PaymentMethodUpdater = () => {
}, [trigger]); }, [trigger]);
return ( return (
<Button <Button onClick={update} loading={isMutating} disabled={isMutating}>
className={styles.button}
onClick={update}
loading={isMutating}
disabled={isMutating}
>
{t['com.affine.payment.billing-setting.update']()} {t['com.affine.payment.billing-setting.update']()}
</Button> </Button>
); );
@@ -492,7 +485,7 @@ const ResumeSubscription = () => {
return ( return (
<ResumeAction open={open} onOpenChange={setOpen}> <ResumeAction open={open} onOpenChange={setOpen}>
<Button className={styles.button} onClick={handleClick}> <Button onClick={handleClick}>
{t['com.affine.payment.billing-setting.resume-subscription']()} {t['com.affine.payment.billing-setting.resume-subscription']()}
</Button> </Button>
</ResumeAction> </ResumeAction>
@@ -503,10 +496,11 @@ const CancelSubscription = ({ loading }: { loading?: boolean }) => {
return ( return (
<IconButton <IconButton
style={{ pointerEvents: 'none' }} style={{ pointerEvents: 'none' }}
icon={<ArrowRightSmallIcon />}
disabled={loading} disabled={loading}
loading={loading} loading={loading}
/> >
<ArrowRightSmallIcon />
</IconButton>
); );
}; };
@@ -583,7 +577,7 @@ const InvoiceLine = ({
: '' : ''
} $${invoice.amount / 100} - ${planText}`} } $${invoice.amount / 100} - ${planText}`}
> >
<Button className={styles.button} onClick={open}> <Button onClick={open}>
{t['com.affine.payment.billing-setting.view-invoice']()} {t['com.affine.payment.billing-setting.view-invoice']()}
</Button> </Button>
</SettingRow> </SettingRow>

View File

@@ -46,9 +46,6 @@ export const currentPlanName = style({
color: cssVar('textEmphasisColor'), color: cssVar('textEmphasisColor'),
cursor: 'pointer', cursor: 'pointer',
}); });
export const button = style({
padding: '4px 12px',
});
export const subscriptionSettingSkeleton = style({ export const subscriptionSettingSkeleton = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@@ -63,7 +63,7 @@ const ExperimentalFeaturesPrompt = ({
<Button <Button
disabled={!checked} disabled={!checked}
onClick={onConfirm} onClick={onConfirm}
type="primary" variant="primary"
data-testid="experimental-confirm-button" data-testid="experimental-confirm-button"
> >
{t[ {t[

View File

@@ -43,12 +43,12 @@ export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
confirmText: confirmText:
t['com.affine.payment.ai.action.cancel.confirm.confirm-text'](), t['com.affine.payment.ai.action.cancel.confirm.confirm-text'](),
confirmButtonOptions: { confirmButtonOptions: {
type: 'default', variant: 'secondary',
}, },
cancelText: cancelText:
t['com.affine.payment.ai.action.cancel.confirm.cancel-text'](), t['com.affine.payment.ai.action.cancel.confirm.cancel-text'](),
cancelButtonOptions: { cancelButtonOptions: {
type: 'primary', variant: 'primary',
}, },
onConfirm: async () => { onConfirm: async () => {
try { try {
@@ -92,7 +92,12 @@ export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
]); ]);
return ( 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']()} {t['com.affine.payment.ai.action.cancel.button-label']()}
</Button> </Button>
); );

View File

@@ -16,7 +16,7 @@ export const AILogin = (btnProps: ButtonProps) => {
}, [setOpen]); }, [setOpen]);
return ( return (
<Button onClick={onClickSignIn} type="primary" {...btnProps}> <Button onClick={onClickSignIn} variant="primary" {...btnProps}>
{t['com.affine.payment.ai.action.login.button-label']()} {t['com.affine.payment.ai.action.login.button-label']()}
</Button> </Button>
); );

View File

@@ -48,7 +48,7 @@ export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
confirmText: confirmText:
t['com.affine.payment.ai.action.resume.confirm.confirm-text'](), t['com.affine.payment.ai.action.resume.confirm.confirm-text'](),
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
}, },
cancelText: cancelText:
t['com.affine.payment.ai.action.resume.confirm.cancel-text'](), 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]); }, [subscription, openConfirmModal, t, module, idempotencyKey]);
return ( 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']()} {t['com.affine.payment.ai.action.resume.button-label']()}
</Button> </Button>
); );

View File

@@ -102,7 +102,7 @@ export const AISubscribe = ({
<Button <Button
loading={isMutating} loading={isMutating}
onClick={subscribe} onClick={subscribe}
type="primary" variant="primary"
{...btnProps} {...btnProps}
> >
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`} {btnProps.children ?? `${priceReadable} / ${priceFrequency}`}

View File

@@ -46,6 +46,7 @@ export const allPlansLink = style({
export const collapsibleHeader = style({ export const collapsibleHeader = style({
display: 'flex', display: 'flex',
alignItems: 'start',
marginBottom: 8, marginBottom: 8,
}); });
export const collapsibleHeaderContent = style({ export const collapsibleHeaderContent = style({

View File

@@ -58,7 +58,7 @@ export const PricingCollapsible = ({
<div className={styles.collapsibleHeaderTitle}>{title}</div> <div className={styles.collapsibleHeaderTitle}>{title}</div>
<div className={styles.collapsibleHeaderCaption}>{caption}</div> <div className={styles.collapsibleHeaderCaption}>{caption}</div>
</div> </div>
<IconButton onClick={toggle}> <IconButton onClick={toggle} size="20">
<ArrowUpSmallIcon <ArrowUpSmallIcon
style={{ style={{
transform: open ? 'rotate(0deg)' : 'rotate(180deg)', transform: open ? 'rotate(0deg)' : 'rotate(180deg)',
@@ -163,7 +163,7 @@ export const PlanLayout = ({ cloud, ai, cloudTip }: PlanLayoutProps) => {
</div> </div>
<Button <Button
onClick={() => scrollToAnchor('cloudPricingPlan')} onClick={() => scrollToAnchor('cloudPricingPlan')}
type="primary" variant="primary"
> >
{t['com.affine.ai-scroll-tip.view']()} {t['com.affine.ai-scroll-tip.view']()}
</Button> </Button>

View File

@@ -48,7 +48,7 @@ export const ConfirmLoadingModal = ({
cancelText={cancelText} cancelText={cancelText}
confirmText={confirmText} confirmText={confirmText}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
loading, loading,
}} }}
open={open} open={open}
@@ -120,7 +120,7 @@ export const DowngradeModal = ({
<Button <Button
disabled={loading} disabled={loading}
onClick={() => onOpenChange?.(false)} onClick={() => onOpenChange?.(false)}
type="primary" variant="primary"
> >
{t['com.affine.payment.modal.downgrade.confirm']()} {t['com.affine.payment.modal.downgrade.confirm']()}
</Button> </Button>

View File

@@ -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 { Tooltip } from '@affine/component/ui/tooltip';
import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify'; import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; 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 { Trans, useI18n } from '@affine/i18n';
import { DoneIcon } from '@blocksuite/icons/rc'; import { DoneIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { HTMLAttributes, PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { authAtom } from '../../../../../atoms/index'; import { authAtom } from '../../../../../atoms/index';
@@ -194,7 +195,7 @@ const Downgrade = ({ disabled }: { disabled?: boolean }) => {
<div className={styles.planAction}> <div className={styles.planAction}>
<Button <Button
className={styles.planAction} className={styles.planAction}
type="primary" variant="primary"
onClick={handleClick} onClick={handleClick}
disabled={disabled} 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']()} {t['com.affine.payment.tell-us-use-case']()}
</Button> </Button>
</a> </a>
@@ -243,8 +244,8 @@ export const Upgrade = ({
className, className,
recurring, recurring,
children, children,
...attrs ...btnProps
}: HTMLAttributes<HTMLButtonElement> & { }: ButtonProps & {
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
}) => { }) => {
const [isMutating, setMutating] = useState(false); const [isMutating, setMutating] = useState(false);
@@ -307,11 +308,11 @@ export const Upgrade = ({
return ( return (
<Button <Button
className={clsx(styles.planAction, className)} className={clsx(styles.planAction, className)}
type="primary" variant="primary"
onClick={upgrade} onClick={upgrade}
disabled={isMutating} disabled={isMutating}
loading={isMutating} loading={isMutating}
{...attrs} {...btnProps}
> >
{children ?? t['com.affine.payment.upgrade']()} {children ?? t['com.affine.payment.upgrade']()}
</Button> </Button>
@@ -371,7 +372,7 @@ const ChangeRecurring = ({
<> <>
<Button <Button
className={styles.planAction} className={styles.planAction}
type="primary" variant="primary"
onClick={onStartChange} onClick={onStartChange}
disabled={disabled || isMutating} disabled={disabled || isMutating}
loading={isMutating} loading={isMutating}
@@ -405,7 +406,7 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
<Button <Button
onClick={onClickSignIn} onClick={onClickSignIn}
className={styles.planAction} className={styles.planAction}
type="primary" variant="primary"
> >
{children} {children}
</Button> </Button>
@@ -415,7 +416,6 @@ const SignUpAction = ({ children }: PropsWithChildren) => {
const ResumeButton = () => { const ResumeButton = () => {
const t = useI18n(); const t = useI18n();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [hovered, setHovered] = useState(false);
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
@@ -435,14 +435,14 @@ const ResumeButton = () => {
return ( return (
<ResumeAction open={open} onOpenChange={setOpen}> <ResumeAction open={open} onOpenChange={setOpen}>
<Button <Button
className={styles.planAction} className={styles.resumeAction}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={handleClick} onClick={handleClick}
style={assignInlineVars({
'--default-content': t['com.affine.payment.current-plan'](),
'--hover-content': t['com.affine.payment.resume-renewal'](),
})}
> >
{hovered <span className={styles.resumeActionContent} />
? t['com.affine.payment.resume-renewal']()
: t['com.affine.payment.current-plan']()}
</Button> </Button>
</ResumeAction> </ResumeAction>
); );

View File

@@ -177,7 +177,17 @@ export const planPriceDesc = style({
}); });
export const planAction = style({ export const planAction = style({
width: '100%', 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({ export const planBenefits = style({
fontSize: cssVar('fontXs'), fontSize: cssVar('fontXs'),

View File

@@ -38,7 +38,7 @@ export const WorkspaceDeleteModal = ({
cancelText={t['com.affine.workspaceDelete.button.cancel']()} cancelText={t['com.affine.workspaceDelete.button.cancel']()}
confirmText={t['com.affine.workspaceDelete.button.delete']()} confirmText={t['com.affine.workspaceDelete.button.delete']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'error', variant: 'error',
disabled: !allowDelete, disabled: !allowDelete,
['data-testid' as string]: 'delete-workspace-confirm-button', ['data-testid' as string]: 'delete-workspace-confirm-button',
}} }}

View File

@@ -136,7 +136,7 @@ export const DeleteLeaveWorkspace = () => {
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()} description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
confirmText={t['Leave']()} confirmText={t['Leave']()}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'warning', variant: 'error',
}} }}
/> />
)} )}

View File

@@ -56,7 +56,7 @@ export const EnableCloudPanel = () => {
> >
<Button <Button
data-testid="publish-enable-affine-cloud-button" data-testid="publish-enable-affine-cloud-button"
type="primary" variant="primary"
onClick={confirmEnableCloudAndClose} onClick={confirmEnableCloudAndClose}
style={{ marginTop: '12px' }} style={{ marginTop: '12px' }}
> >

View File

@@ -63,7 +63,7 @@ const MembersPanelLocal = () => {
<Tooltip content={t['com.affine.settings.member-tooltip']()}> <Tooltip content={t['com.affine.settings.member-tooltip']()}>
<div className={style.fakeWrapper}> <div className={style.fakeWrapper}>
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}> <SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
<Button size="large">{t['Invite Members']()}</Button> <Button>{t['Invite Members']()}</Button>
</SettingRow> </SettingRow>
</div> </div>
</Tooltip> </Tooltip>
@@ -393,7 +393,6 @@ const MemberItem = ({
> >
<IconButton <IconButton
disabled={!operationButtonInfo.show} disabled={!operationButtonInfo.show}
type="plain"
style={{ style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden', visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0, flexShrink: 0,

View File

@@ -173,11 +173,9 @@ const EditPropertyButton = ({
}} }}
items={editing ? editMenuItems : defaultMenuItems} items={editing ? editMenuItems : defaultMenuItems}
> >
<IconButton <IconButton onClick={() => setOpen(true)} size="20">
onClick={() => setOpen(true)} <MoreHorizontalIcon />
type="plain" </IconButton>
icon={<MoreHorizontalIcon />}
/>
</Menu> </Menu>
<ConfirmDeletePropertyModal <ConfirmDeletePropertyModal
onConfirm={() => { onConfirm={() => {
@@ -350,7 +348,7 @@ const WorkspaceSettingPropertiesMain = () => {
<div className={styles.listHeader}> <div className={styles.listHeader}>
{properties.length > 0 ? ( {properties.length > 0 ? (
<Menu items={filterMenuItems}> <Menu items={filterMenuItems}>
<Button type="default" icon={<FilterIcon />}> <Button prefix={<FilterIcon />}>
{filterMode === 'all' {filterMode === 'all'
? t['com.affine.filter']() ? t['com.affine.filter']()
: t[`com.affine.settings.workspace.properties.${filterMode}`]()} : 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']()} {t['com.affine.settings.workspace.properties.add_property']()}
</Button> </Button>
</Menu> </Menu>

View File

@@ -55,7 +55,7 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
}, [shareService]); }, [shareService]);
return ( return (
<Button ref={ref} className={styles.shareButton} type="primary"> <Button ref={ref} className={styles.shareButton} variant="primary">
{shared {shared
? t['com.affine.share-menu.sharedButton']() ? t['com.affine.share-menu.sharedButton']()
: t['com.affine.share-menu.shareButton']()} : t['com.affine.share-menu.shareButton']()}

View File

@@ -40,7 +40,7 @@ export const LocalSharePage = (props: ShareMenuProps) => {
<div> <div>
<Button <Button
onClick={props.onEnableAffineCloud} onClick={props.onEnableAffineCloud}
type="primary" variant="primary"
data-testid="share-menu-enable-affine-cloud-button" data-testid="share-menu-enable-affine-cloud-button"
> >
{t['Enable AFFiNE Cloud']()} {t['Enable AFFiNE Cloud']()}
@@ -256,7 +256,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
) : ( ) : (
<Button <Button
onClick={onClickCreateLink} onClick={onClickCreateLink}
type="primary" variant="primary"
data-testid="share-menu-create-link-button" data-testid="share-menu-create-link-button"
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }} style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
> >

View File

@@ -32,7 +32,7 @@ export const SignOutModal = ({ ...props }: ConfirmModalProps) => {
cancelText={cancelText ?? defaultTexts.cancelText} cancelText={cancelText ?? defaultTexts.cancelText}
confirmText={confirmText ?? defaultTexts.children} confirmText={confirmText ?? defaultTexts.children}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'error', variant: 'error',
['data-testid' as string]: 'confirm-sign-out-button', ['data-testid' as string]: 'confirm-sign-out-button',
}} }}
contentOptions={{ contentOptions={{

View File

@@ -26,7 +26,7 @@ export const StarAFFiNEModal = () => {
cancelText={t['com.affine.star-affine.cancel']()} cancelText={t['com.affine.star-affine.cancel']()}
to={runtimeConfig.githubUrl} to={runtimeConfig.githubUrl}
confirmButtonOptions={{ confirmButtonOptions={{
type: 'primary', variant: 'primary',
}} }}
confirmText={t['com.affine.star-affine.confirm']()} confirmText={t['com.affine.star-affine.confirm']()}
external external

View File

@@ -48,7 +48,7 @@ const UpgradeSuccessLayout = ({
return ( return (
<AuthPageContainer title={title} subtitle={subtitle}> <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']()} {t['com.affine.other-page.nav.open-affine']()}
</Button> </Button>
</AuthPageContainer> </AuthPageContainer>

View File

@@ -32,7 +32,7 @@ const SubscriptionChangedNotifyFooter = ({
className={clsx(actionButton, cancelButton)} className={clsx(actionButton, cancelButton)}
size={'default'} size={'default'}
onClick={onCancel} onClick={onCancel}
type="plain" variant="plain"
> >
{cancelText} {cancelText}
</Button> </Button>
@@ -40,7 +40,7 @@ const SubscriptionChangedNotifyFooter = ({
<Button <Button
onClick={onConfirm} onClick={onConfirm}
className={clsx(actionButton, confirmButton)} className={clsx(actionButton, confirmButton)}
type="plain" variant="plain"
> >
{okText} {okText}
</Button> </Button>

View File

@@ -6,11 +6,7 @@ export const root = style({
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.15)', 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'), background: cssVarV2('button/siderbarPrimary/background'),
}); });
export const icon = style({
color: cssVarV2('icon/primary'),
fontSize: 20,
display: 'block',
});

View File

@@ -1,4 +1,4 @@
import { Button, Tooltip } from '@affine/component'; import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -12,6 +12,7 @@ interface AddPageButtonProps {
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const sideBottom = { side: 'bottom' as const };
export function AddPageButton({ export function AddPageButton({
onClick, onClick,
className, className,
@@ -20,15 +21,15 @@ export function AddPageButton({
const t = useI18n(); const t = useI18n();
return ( return (
<Tooltip content={t['New Page']()} side="bottom"> <IconButton
<Button tooltip={t['New Page']()}
data-testid="sidebar-new-page-button" tooltipOptions={sideBottom}
style={style} data-testid="sidebar-new-page-button"
className={clsx([styles.root, className])} style={style}
onClick={onClick} className={clsx([styles.root, className])}
> onClick={onClick}
<PlusIcon className={styles.icon} /> >
</Button> <PlusIcon />
</Tooltip> </IconButton>
); );
} }

View File

@@ -34,25 +34,20 @@ export const label = style({
lineHeight: '20px', lineHeight: '20px',
flexGrow: '0', flexGrow: '0',
display: 'flex', display: 'flex',
gap: 2,
alignItems: 'center', alignItems: 'center',
justifyContent: 'start', justifyContent: 'start',
cursor: 'pointer', cursor: 'pointer',
}); });
export const collapseButton = style({
selectors: {
[`${label} > &`]: {
color: cssVarV2('icon/tertiary'),
transform: 'translateY(1px)',
},
},
});
export const collapseIcon = style({ 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', transition: 'transform 0.2s',
selectors: { selectors: {
[`${root}[data-collapsed="true"] &`]: { [`${root}[data-collapsed="true"] &`]: {
transform: 'rotate(0deg)', vars: { '--r': '0deg' },
}, },
}, },
}); });

View File

@@ -1,4 +1,3 @@
import { IconButton } from '@affine/component';
import { ToggleCollapseIcon } from '@blocksuite/icons/rc'; import { ToggleCollapseIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
import { type ForwardedRef, forwardRef, type PropsWithChildren } from 'react'; import { type ForwardedRef, forwardRef, type PropsWithChildren } from 'react';
@@ -42,14 +41,12 @@ export const CategoryDivider = forwardRef(
<div className={styles.label}> <div className={styles.label}>
{label} {label}
{collapsible ? ( {collapsible ? (
<IconButton <ToggleCollapseIcon
withoutHoverStyle width={16}
className={styles.collapseButton} height={16}
size="small"
data-testid="category-divider-collapse-button" data-testid="category-divider-collapse-button"
> className={styles.collapseIcon}
<ToggleCollapseIcon className={styles.collapseIcon} /> />
</IconButton>
) : null} ) : null}
</div> </div>
<div className={styles.actions} onClick={e => e.stopPropagation()}> <div className={styles.actions} onClick={e => e.stopPropagation()}>

View File

@@ -1,23 +1,18 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const sidebarSwitch = style({ export const sidebarSwitchClip = style({
opacity: 0, flexShrink: 0,
display: 'inline-flex',
overflow: 'hidden', overflow: 'hidden',
pointerEvents: 'none', transition:
transition: 'max-width 0.2s ease-in-out, margin 0.3s ease-in-out', 'max-width 0.2s ease-in-out, margin 0.3s ease-in-out, opacity 0.3s ease',
selectors: { selectors: {
'&[data-show=true]': { '&[data-show=true]': {
maxWidth: '32px',
opacity: 1, opacity: 1,
width: '32px', maxWidth: '60px',
flexShrink: 0,
fontSize: '24px',
pointerEvents: 'auto',
}, },
'&[data-show=false]': { '&[data-show=false]': {
opacity: 0,
maxWidth: 0, maxWidth: 0,
margin: '0 !important',
}, },
}, },
}); });

View File

@@ -1,7 +1,6 @@
import { IconButton, Tooltip } from '@affine/component'; import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { SidebarIcon } from '@blocksuite/icons/rc'; import { SidebarIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { appSidebarOpenAtom } from '../index.jotai'; import { appSidebarOpenAtom } from '../index.jotai';
@@ -19,19 +18,21 @@ export const SidebarSwitch = ({
const tooltipContent = open const tooltipContent = open
? t['com.affine.sidebarSwitch.collapse']() ? t['com.affine.sidebarSwitch.collapse']()
: t['com.affine.sidebarSwitch.expand'](); : t['com.affine.sidebarSwitch.expand']();
// TODO(@CatsJuice): Tooltip shortcut style
const collapseKeyboardShortcuts = const collapseKeyboardShortcuts =
environment.isBrowser && environment.isMacOs ? ' ⌘+/' : ' Ctrl+/'; environment.isBrowser && environment.isMacOs ? ' ⌘+/' : ' Ctrl+/';
return ( return (
<Tooltip <div
content={tooltipContent + ' ' + collapseKeyboardShortcuts} data-show={show}
side={open ? 'bottom' : 'right'} className={styles.sidebarSwitchClip}
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
> >
<IconButton <IconButton
className={clsx(styles.sidebarSwitch, className)} tooltip={tooltipContent + ' ' + collapseKeyboardShortcuts}
data-show={show} tooltipOptions={{ side: open ? 'bottom' : 'right' }}
size="large" className={className}
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`} size="24"
style={{ style={{
zIndex: 1, zIndex: 1,
}} }}
@@ -39,6 +40,6 @@ export const SidebarSwitch = ({
> >
<SidebarIcon /> <SidebarIcon />
</IconButton> </IconButton>
</Tooltip> </div>
); );
}; };

View File

@@ -134,7 +134,7 @@ export function patchNotificationService(
description: toReactNode(message), description: toReactNode(message),
confirmText, confirmText,
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
}, },
cancelText, cancelText,
onConfirm: () => { onConfirm: () => {
@@ -177,7 +177,7 @@ export function patchNotificationService(
description: description, description: description,
confirmText: confirmText ?? 'Confirm', confirmText: confirmText ?? 'Confirm',
confirmButtonOptions: { confirmButtonOptions: {
type: 'primary', variant: 'primary',
}, },
cancelText: cancelText ?? 'Cancel', cancelText: cancelText ?? 'Cancel',
onConfirm: () => { onConfirm: () => {

View File

@@ -1,4 +1,4 @@
import { IconButton, Tooltip } from '@affine/component'; import { IconButton } from '@affine/component';
import { openInfoModalAtom } from '@affine/core/atoms'; import { openInfoModalAtom } from '@affine/core/atoms';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { InformationIcon } from '@blocksuite/icons/rc'; import { InformationIcon } from '@blocksuite/icons/rc';
@@ -11,12 +11,13 @@ export const InfoButton = () => {
setOpenInfoModal(true); setOpenInfoModal(true);
}; };
return ( return (
<Tooltip content={t['com.affine.page-properties.page-info.view']()}> <IconButton
<IconButton size="20"
data-testid="header-info-button" tooltip={t['com.affine.page-properties.page-info.view']()}
onClick={onOpenInfoModal} data-testid="header-info-button"
icon={<InformationIcon />} onClick={onOpenInfoModal}
/> >
</Tooltip> <InformationIcon />
</IconButton>
); );
}; };

View File

@@ -9,9 +9,10 @@ export const DetailPageHeaderPresentButton = () => {
return ( return (
<IconButton <IconButton
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
size={'large'} size="24"
icon={<PresentationIcon />}
onClick={() => handlePresent(!isPresent)} onClick={() => handlePresent(!isPresent)}
></IconButton> >
<PresentationIcon />
</IconButton>
); );
}; };

View File

@@ -11,11 +11,10 @@ export const PresentButton = () => {
return ( return (
<Button <Button
icon={<PresentationIcon />} prefix={<PresentationIcon />}
className={styles.presentButton} className={styles.presentButton}
onClick={() => handlePresent()} onClick={() => handlePresent()}
disabled={isPresent} disabled={isPresent}
withoutHoverStyle
> >
{t['com.affine.share-page.header.present']()} {t['com.affine.share-page.header.present']()}
</Button> </Button>

View File

@@ -46,7 +46,6 @@ export const headerDivider = style({
}); });
export const presentButton = style({ export const presentButton = style({
gap: '4px',
background: cssVar('black'), background: cssVar('black'),
color: cssVar('white'), color: cssVar('white'),
borderColor: cssVar('pureBlack10'), borderColor: cssVar('pureBlack10'),

View File

@@ -1,7 +1,8 @@
import type { IconButtonProps } from '@affine/component'; import type { IconButtonProps } from '@affine/component';
import { IconButton, Tooltip } from '@affine/component'; import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons/rc'; import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
import Lottie from 'lottie-react'; import Lottie from 'lottie-react';
import { forwardRef, useCallback, useState } from 'react'; import { forwardRef, useCallback, useState } from 'react';
@@ -25,24 +26,32 @@ export const FavoriteTag = forwardRef<
[active, onClick] [active, onClick]
); );
return ( return (
<Tooltip content={active ? t['Favorited']() : t['Favorite']()} side="top"> <IconButton
<IconButton ref={ref} active={active} onClick={handleClick} {...props}> tooltip={active ? t['Favorited']() : t['Favorite']()}
{active ? ( tooltipOptions={{ side: 'top' }}
playAnimation ? ( ref={ref}
<Lottie onClick={handleClick}
loop={false} size="20"
animationData={favoritedAnimation} {...props}
onComplete={() => setPlayAnimation(false)} >
style={{ width: '20px', height: '20px' }} {active ? (
/> playAnimation ? (
) : ( <Lottie
<FavoritedIcon data-testid="favorited-icon" /> loop={false}
) animationData={favoritedAnimation}
onComplete={() => setPlayAnimation(false)}
style={{ width: '20px', height: '20px' }}
/>
) : ( ) : (
<FavoriteIcon /> <FavoritedIcon
)} color={cssVar('primaryColor')}
</IconButton> data-testid="favorited-icon"
</Tooltip> />
)
) : (
<FavoriteIcon />
)}
</IconButton>
); );
}); });
FavoriteTag.displayName = 'FavoriteTag'; FavoriteTag.displayName = 'FavoriteTag';

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