Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[#67] - 버튼 공통 컴포넌트 디자인 시스템 적용 #69

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"plugins": ["stylelint-order"],
"rules": {
"no-descending-specificity": null,
"no-empty-source": null,
"order/properties-order": [
"display",
"align-items",
Expand Down
140 changes: 140 additions & 0 deletions src/assets/styles/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ReactNode } from 'react';

import { BaseButtonProps } from '@/common/components/button/base-button';

import { colors } from './colors';
import { sizeToken } from './space';

export const BUTTON_SIZE = {
text: {
small: {
padding: `${sizeToken.padding['xs_2']} ${sizeToken.padding['sm_2']}`,
fontSize: `${sizeToken.fontSize['xs_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
medium: {
padding: `${sizeToken.padding['s_2']} ${sizeToken.padding['md_2']}`,
fontSize: `${sizeToken.fontSize['s_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
large: {
padding: `${sizeToken.padding['sm_2']} ${sizeToken.padding['lg_1']}`,
fontSize: `${sizeToken.fontSize['s_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
},
icon: {
small: {
padding: `${sizeToken.padding['s_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
medium: {
padding: `${sizeToken.padding['s_2']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
large: {
padding: `${sizeToken.padding['sm_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
},
},
multi: {
small: {
padding: `${sizeToken.padding['xs_2']} ${sizeToken.padding['sm_2']} ${sizeToken.padding['xs_2']} ${sizeToken.padding['s_2']}`,
fontSize: `${sizeToken.fontSize['xs_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
gap: `${sizeToken.gap['xs_1']}`,
},
medium: {
padding: `${sizeToken.padding['s_2']} ${sizeToken.padding['md_2']} ${sizeToken.padding['s_2']} ${sizeToken.padding['md_1']}`,
fontSize: `${sizeToken.fontSize['s_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
gap: `${sizeToken.gap['xs_1']}`,
},
large: {
padding: `${sizeToken.padding['sm_2']} ${sizeToken.padding['lg_1']} ${sizeToken.padding['sm_2']} ${sizeToken.padding['md_2']}`,
fontSize: `${sizeToken.fontSize['s_1']}`,
borderRadius: `${sizeToken.borderRadius['xs_1']}`,
lineHeight: `${sizeToken.lineHeight['xs_1']}`,
gap: `${sizeToken.gap['xs_1']}`,
},
},
};

export const BUTTON_VARIANTS = {
purlple: {
default: {
background: colors.PURPLE[300],
color: colors.GRAY[950],
},
hover: {
background: colors.PURPLE[200],
color: colors.GRAY[950],
},
pressed: {
background: colors.PURPLE[400],
color: colors.GRAY[950],
},
disabled: {
background: colors.PURPLE[400],
color: colors.GRAY[700],
},
},
sora: {
default: {
background: colors.SORA[200],
color: colors.GRAY[950],
},
hover: {
background: colors.SORA[100],
color: colors.GRAY[950],
},
pressed: {
background: colors.SORA[400],
color: colors.GRAY[950],
},
disabled: {
background: colors.SORA[400],
color: colors.GRAY[700],
},
},
};

// 각 카테고리별 사이즈 키
export type Usage = keyof typeof BUTTON_SIZE;
export type Size = keyof (typeof BUTTON_SIZE)['text'];
export type ButtonVariant = keyof typeof BUTTON_VARIANTS;

// 공통 props
export type CommonProps = {
size: Size;
variant: ButtonVariant;
} & BaseButtonProps;

// 텍스트만 있는 경우
export type TextOnlyProps = CommonProps & {
usage: 'text';
children: ReactNode;
icon?: never;
iconPosition?: never;
};

// 텍스트와 아이콘이 모두 있는 경우
export type TextAndIconProps = CommonProps & {
usage: 'multi';
children: ReactNode;
icon: ReactNode;
iconPosition: 'left' | 'right';
};

// 아이콘만 있는 경우
export type IconOnlyProps = CommonProps & {
usage: 'icon';
icon: ReactNode;
children?: never;
iconPosition?: never;
};
2 changes: 2 additions & 0 deletions src/assets/styles/emotion.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import '@emotion/react';
import { ColorsTypes } from '@/assets/styles/colors';

import { FontsTypes } from './fonts';
import { SizeTypes } from './space';

declare module '@emotion/react' {
export interface Theme {
fonts: FontsTypes;
colors: ColorsTypes;
sizeToken: SizeTypes;
}
}
28 changes: 28 additions & 0 deletions src/assets/styles/space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const sizeToken = {
padding: {
xs_1: '0.4rem',
xs_2: '0.7rem',
s_1: '0.8rem',
s_2: '1.2rem',
sm_1: '1.4rem',
sm_2: '1.6rem',
md_1: '2rem',
md_2: '2.4rem',
lg_1: '3.2rem',
},
fontSize: {
xs_1: '1.4rem',
s_1: '1.6rem',
},
borderRadius: {
xs_1: '0.8rem',
},
lineHeight: {
xs_1: '127%',
},
gap: {
xs_1: '0.4rem',
},
} as const;

export type SizeTypes = typeof sizeToken;
2 changes: 2 additions & 0 deletions src/assets/styles/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Theme } from '@emotion/react';
import { colors } from '@/assets/styles/colors';

import { fonts } from './fonts';
import { sizeToken } from './space';

export const theme: Theme = {
fonts,
colors,
sizeToken,
};

export type ThemeType = typeof theme;
26 changes: 14 additions & 12 deletions src/common/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { forwardRef } from 'react';

import { BaseButton, BaseButtonProps } from '@/common/components/base-button/base-button';
import { IconOnlyProps, TextAndIconProps, TextOnlyProps } from '@/assets/styles/button';
import { BaseButton } from '@/common/components/button/base-button';

import * as styles from './button.styles';

/** TODO: 디자인 시스템에 따라 추가 및 세분화 예정 */
export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
export type ButtonSize = 'default' | 'sm' | 'lg' | 'icon';

/** 디자인 시스템을 따르는 버튼 */
export interface ButtonProps extends BaseButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
}
export type ButtonProps = TextOnlyProps | TextAndIconProps | IconOnlyProps;

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'default', size = 'default', ...props }, ref) => {
return <BaseButton css={[styles.sizes[size], styles.variants[variant]]} ref={ref} {...props} />;
({ size, usage, variant, icon, children, iconPosition, ...props }, ref) => {
const content = usage !== 'icon' ? children : icon;

return (
<BaseButton ref={ref} css={styles.buttonsStyle(size, usage, variant)} {...props}>
{usage === 'multi' && iconPosition === 'left' && icon}
{content}
{usage === 'multi' && iconPosition === 'right' && icon}
</BaseButton>
);
},
);

Button.displayName = 'Button';

export { Button };
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ export interface BaseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
asChild?: boolean;
}

/** 기본 버튼 스타일 & 주입받은 props를 기반으로 UI를 렌더링하는 컴포넌트 */
/** 기본 버튼 스타일 & 주입받은 props와 아이콘을 기반으로 최종 UI를 렌더링하는 컴포넌트 */
/** 상황에 따라 주입받은 스타일이 기본 버튼 스타일에 대한 오버라이딩 가능 */
const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(({ asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp css={styles.button} ref={ref} {...props} />;

return <Comp css={styles.button} ref={ref} {...props}></Comp>;
});
BaseButton.displayName = 'BaseButton';

Expand Down
63 changes: 63 additions & 0 deletions src/common/components/button/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Meta, StoryObj } from '@storybook/react';

import { Button } from './Button';
import Icon from '../icon/icon';

const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
size: {
description: '버튼의 크기를 결정합니다.',
control: { type: 'radio' },
options: ['small', 'medium', 'large'],
},
variant: {
description: '버튼의 색상 테마를 결정합니다.',
control: { type: 'radio' },
options: ['purlple', 'sora'],
},
usage: {
table: { disable: true },
},
},
};

export default meta;

type Story = StoryObj<typeof Button>;

export const TextButton: Story = {
args: {
variant: 'sora',
children: 'hello',
usage: 'text',
size: 'small',
},
};
export const IconButton: Story = {
args: {
variant: 'sora',
usage: 'icon',
size: 'small',
icon: <Icon name="pin" width={24} />,
},
};

export const MultiButton: Story = {
args: {
variant: 'purlple',
children: 'hello',
usage: 'multi',
iconPosition: 'left',
size: 'small',
icon: <Icon name="pin" width={24} />,
},
argTypes: {
iconPosition: {
description: '아이콘의 위치를 조정합니다.',
control: { type: 'radio' },
options: ['left', 'right'],
},
},
};
Loading