mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
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

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