mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00: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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user