├── .babelrc ├── .gitignore ├── .storybook ├── addons.js ├── config.js ├── presets.js └── webpack.config.js ├── README.md ├── package.json ├── rollup.config.js ├── src ├── Button │ ├── Button.stories.tsx │ └── Button.tsx ├── ButtonGroup │ ├── ButtonGroup.stories.tsx │ └── ButtonGroup.tsx ├── Dialog │ ├── Dialog.stories.tsx │ └── Dialog.tsx ├── Icon │ ├── Icon.stories.tsx │ ├── Icon.tsx │ └── svg │ │ ├── exit.svg │ │ ├── heart.svg │ │ ├── index.ts │ │ └── pencil.svg ├── index.ts └── typings.d.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["react-app", { "flow": false, "typescript": true }]] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | storybook-static -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-knobs/register'; 4 | import '@storybook/addon-viewport/register'; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | configure(require.context('../src', true, /\.stories\.(js|mdx|tsx)$/), module); 5 | -------------------------------------------------------------------------------- /.storybook/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = ['@storybook/addon-docs/react/preset']; 2 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve('babel-loader'), 7 | options: { 8 | presets: [['react-app', { flow: false, typescript: true }]], 9 | plugins: [ 10 | [ 11 | require.resolve('babel-plugin-named-asset-import'), 12 | { 13 | loaderMap: { 14 | svg: { 15 | ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]' 16 | } 17 | } 18 | } 19 | ] 20 | ] 21 | } 22 | }, 23 | require.resolve('react-docgen-typescript-loader') 24 | ] 25 | }); 26 | config.resolve.extensions.push('.ts', '.tsx'); 27 | return config; 28 | }; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-uikit-sample 2 | 3 | A Sample React UI Kit. 4 | 5 | [Storybook](https://react-uikit-sample.surge.sh) 6 | 7 | Used tools: 8 | 9 | - TypeScript 10 | - Storybook 11 | - Rollup 12 | 13 | ## Tags 14 | 15 | 각 섹션별로 Git Repository 의 태그가 생성되어 있습니다. 16 | 17 | - [2-1. Knobs Addon](https://github.com/velopert/storybook-tutorial-code/tree/02a) 18 | - [2-2. Actions 애드온](https://github.com/velopert/storybook-tutorial-code/tree/02b) 19 | - [2-3. Docs 애드온](https://github.com/velopert/storybook-tutorial-code/tree/02c) 20 | - [3. TypeScript 사용하기](https://github.com/velopert/storybook-tutorial-code/tree/03) 21 | - [4-1. Button 만들기](https://github.com/velopert/storybook-tutorial-code/tree/04a) 22 | - [4-2. ButtonGroup 만들기](https://github.com/velopert/storybook-tutorial-code/tree/04b) 23 | - [4-3. Icon 만들기](https://github.com/velopert/storybook-tutorial-code/tree/04c) 24 | - [4-4. Dialog 만들기](https://github.com/velopert/storybook-tutorial-code/tree/04d) 25 | - [5-1. Rollup으로 라이브러리 번들하기](https://github.com/velopert/storybook-tutorial-code/tree/05a) 26 | - [5-2. npm에 publish 하기](https://github.com/velopert/storybook-tutorial-code/tree/05b) 27 | 28 | 프로젝트를 clone 했을 경우 다음 명령어를 사용하여 특정 태그의 코드를 볼 수 있습니다. 29 | 30 | ``` 31 | git checkout 02a 32 | ``` 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-uikit-sample", 3 | "version": "1.0.3", 4 | "module": "dist/index.js", 5 | "license": "MIT", 6 | "types": "dist/types/index.d.ts", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "dependencies": { 11 | "react-spring": "^8.0.27" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.7.2", 15 | "@storybook/addon-actions": "^5.2.6", 16 | "@storybook/addon-docs": "^5.2.6", 17 | "@storybook/addon-knobs": "^5.2.6", 18 | "@storybook/addon-links": "^5.2.6", 19 | "@storybook/addon-viewport": "^5.2.6", 20 | "@storybook/addons": "^5.2.6", 21 | "@storybook/react": "^5.2.6", 22 | "@svgr/rollup": "^4.3.3", 23 | "babel-loader": "^8.0.6", 24 | "babel-plugin-named-asset-import": "^0.3.4", 25 | "babel-preset-react-app": "^9.0.2", 26 | "react-docgen-typescript-loader": "^3.4.0", 27 | "rollup": "^1.27.2", 28 | "rollup-plugin-babel": "^4.3.3", 29 | "rollup-plugin-commonjs": "^10.1.0", 30 | "rollup-plugin-node-resolve": "^5.2.0", 31 | "rollup-plugin-peer-deps-external": "^2.2.0", 32 | "rollup-plugin-url": "^3.0.0", 33 | "typescript": "^3.7.2" 34 | }, 35 | "scripts": { 36 | "storybook": "start-storybook -p 6006", 37 | "build-storybook": "build-storybook", 38 | "build": "rollup -c", 39 | "build:types": "tsc --emitDeclarationOnly" 40 | }, 41 | "peerDependencies": { 42 | "@emotion/core": "^10.0.22", 43 | "react": "^16.12.0", 44 | "react-dom": "^16.12.0", 45 | "react-spring": "^8.0.27" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import pkg from './package.json'; 5 | import external from 'rollup-plugin-peer-deps-external'; 6 | import svgr from '@svgr/rollup'; 7 | import url from 'rollup-plugin-url'; 8 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 9 | 10 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; // 어떤 확장자를 처리 할 지 정함 11 | 12 | // babel-preset-react-app를 사용한다면 BABEL_ENV를 필수로 설정해야함. 13 | process.env.BABEL_ENV = 'production'; 14 | 15 | export default { 16 | input: './src/index.ts', // 어떤 파일부터 불러올지 정함. 17 | plugins: [ 18 | peerDepsExternal() /* peerDependencies로 설치한 라이브러리들을 external 모듈로 설정 19 | 즉, 번들링된 결과에 포함시키지 않음 */, 20 | resolve({ extensions }), // node_modules 에서 모듈을 불러올 수 있게 해줌. ts/tsx 파일도 불러올 수 있게 해줌 21 | commonjs({ 22 | include: 'node_modules/**' 23 | }), // CommonJS 형태로 만들어진 모듈도 불러와서 사용 할 수 있게 해줌. 현재 프로젝트 상황에서는 없어도 무방함 24 | babel({ extensions, include: ['src/**/*'], runtimeHelpers: true }), // Babel을 사용 할 수 있게 해줌 25 | url(), // 미디어 파일을 dataURI 형태로 불러와서 사용 할 수 있게 해줌. 26 | svgr() // SVG를 컴포넌트로 사용 할 수 있게 해줌. 27 | ], 28 | output: [ 29 | { 30 | file: pkg.module, // 번들링한 파일을 저장 할 경로 31 | format: 'es' // ES Module 형태로 번들링함 32 | } 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /src/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import Button from './Button'; 3 | import { jsx, css } from '@emotion/core'; 4 | import { withKnobs, text, boolean, select } from '@storybook/addon-knobs'; 5 | import { action } from '@storybook/addon-actions'; 6 | import ButtonGroup from '../ButtonGroup/ButtonGroup'; 7 | import Icon from '../Icon/Icon'; 8 | 9 | export default { 10 | title: 'components|Button', 11 | component: Button, 12 | decorators: [withKnobs] 13 | }; 14 | 15 | export const button = () => { 16 | const label = text('children', 'BUTTON'); 17 | const size = select('size', ['small', 'medium', 'big'], 'medium'); 18 | const theme = select( 19 | 'theme', 20 | ['primary', 'secondary', 'tertiary'], 21 | 'primary' 22 | ); 23 | const disabled = boolean('disabled', false); 24 | const width = text('width', ''); 25 | 26 | return ( 27 | 36 | ); 37 | }; 38 | 39 | button.story = { 40 | name: 'Default' 41 | }; 42 | 43 | export const primaryButton = () => { 44 | return ; 45 | }; 46 | 47 | export const secondaryButton = () => { 48 | return ; 49 | }; 50 | 51 | export const tertiaryButton = () => { 52 | return ; 53 | }; 54 | 55 | const buttonWrapper = css` 56 | .description { 57 | margin-bottom: 0.5rem; 58 | } 59 | & > div + div { 60 | margin-top: 2rem; 61 | } 62 | `; 63 | 64 | export const sizes = () => { 65 | return ( 66 |
67 |
68 |
Small
69 | 70 |
71 |
72 |
Medium
73 | 74 |
75 |
76 |
Big
77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export const disabled = () => { 84 | return ( 85 |
86 |
87 | 88 |
89 |
90 | 93 |
94 |
95 | 98 |
99 |
100 | ); 101 | }; 102 | 103 | export const customSized = () => { 104 | return ( 105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | ); 114 | }; 115 | 116 | export const withIcon = () => { 117 | return ( 118 |
119 | 120 | 123 | 126 | 129 | 130 |
131 | ); 132 | }; 133 | 134 | export const iconOnly = () => { 135 | return ( 136 |
137 | 138 | 141 | 144 | 147 | 148 |
149 | ); 150 | }; 151 | -------------------------------------------------------------------------------- /src/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx, css } from '@emotion/core'; 3 | 4 | type ButtonProps = { 5 | /** 버튼 안의 내용 */ 6 | children: React.ReactNode; 7 | /** 클릭했을 때 호출할 함수 */ 8 | onClick?: (e?: React.MouseEvent) => void; 9 | /** 버튼의 생김새를 설정합니다. */ 10 | theme: 'primary' | 'secondary' | 'tertiary'; 11 | /** 버튼의 크기를 설정합니다. */ 12 | size: 'small' | 'medium' | 'big'; 13 | /** 버튼을 비활성화 시킵니다. */ 14 | disabled?: boolean; 15 | /** 버튼의 너비를 임의로 설정합니다. */ 16 | width?: string | number; 17 | /** 버튼에서 아이콘만 보여줄 때 이 값을 `true`로 설정하세요. */ 18 | iconOnly?: boolean; 19 | }; 20 | 21 | /** `Button` 컴포넌트는 어떠한 작업을 트리거 할 때 사용합니다. */ 22 | const Button = ({ 23 | children, 24 | theme, 25 | size, 26 | disabled, 27 | width, 28 | iconOnly, 29 | onClick 30 | }: ButtonProps) => { 31 | return ( 32 | 45 | ); 46 | }; 47 | 48 | Button.defaultProps = { 49 | theme: 'primary', 50 | size: 'medium' 51 | }; 52 | 53 | const style = css` 54 | outline: none; 55 | border: none; 56 | box-sizing: border-box; 57 | border-radius: 0.25rem; 58 | line-height: 1; 59 | font-weight: 600; 60 | display: inline-flex; 61 | align-items: center; 62 | justify-content: center; 63 | &:focus { 64 | box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.2); 65 | } 66 | &:disabled { 67 | cursor: not-allowed; 68 | } 69 | svg { 70 | width: 1em; 71 | margin-right: 1em; 72 | } 73 | `; 74 | 75 | const themes = { 76 | primary: css` 77 | background: #20c997; 78 | color: white; 79 | svg { 80 | fill: white; 81 | } 82 | &:hover:enabled { 83 | background: #38d9a9; 84 | } 85 | &:active:enabled { 86 | background: #12b886; 87 | } 88 | &:disabled { 89 | background: #aed9cc; 90 | } 91 | `, 92 | secondary: css` 93 | background: #e9ecef; 94 | color: #343a40; 95 | svg { 96 | fill: #343a40; 97 | } 98 | &:hover:enabled { 99 | background: #f1f3f5; 100 | } 101 | &:active:enabled { 102 | background: #dee2e6; 103 | } 104 | &:disabled { 105 | color: #c6d3e1; 106 | svg { 107 | fill: #c6d3e1; 108 | } 109 | } 110 | `, 111 | tertiary: css` 112 | background: none; 113 | color: #20c997; 114 | svg { 115 | fill: #20c997; 116 | } 117 | &:hover:enabled { 118 | background: #e6fcf5; 119 | } 120 | &:active:enabled { 121 | background: #c3fae8; 122 | } 123 | &:disabled { 124 | color: #bcd9d0; 125 | svg { 126 | fill: #bcd9d0; 127 | } 128 | } 129 | ` 130 | }; 131 | 132 | const sizes = { 133 | small: css` 134 | height: 1.75rem; 135 | font-size: 0.75rem; 136 | padding: 0 0.875rem; 137 | `, 138 | medium: css` 139 | height: 2.5rem; 140 | font-size: 1rem; 141 | padding: 0 1rem; 142 | `, 143 | big: css` 144 | height: 3rem; 145 | font-size: 1.125rem; 146 | padding: 0 1.5rem; 147 | ` 148 | }; 149 | 150 | const iconOnlyStyle = css` 151 | padding: 0; 152 | border-radius: 50%; 153 | svg { 154 | margin: 0; 155 | } 156 | `; 157 | 158 | const iconOnlySizes = { 159 | small: css` 160 | width: 1.75rem; 161 | `, 162 | medium: css` 163 | width: 2.5rem; 164 | `, 165 | big: css` 166 | width: 3rem; 167 | ` 168 | }; 169 | 170 | export default Button; 171 | -------------------------------------------------------------------------------- /src/ButtonGroup/ButtonGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ButtonGroup from './ButtonGroup'; 3 | import Button from '../Button/Button'; 4 | import { withKnobs, text, radios, boolean } from '@storybook/addon-knobs'; 5 | 6 | export default { 7 | title: 'components|ButtonGroup', 8 | component: ButtonGroup, 9 | decorators: [withKnobs] 10 | }; 11 | 12 | export const buttonGroup = () => { 13 | const direction = radios( 14 | 'direction', 15 | { Row: 'row', Column: 'column' }, 16 | 'row' 17 | ); 18 | const rightAlign = boolean('rightAlign', false); 19 | const gap = text('gap', '0.5rem'); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | buttonGroup.story = { 30 | name: 'Default' 31 | }; 32 | 33 | export const rightAlign = () => { 34 | return ( 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export const column = () => { 43 | return ( 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export const customGap = () => { 52 | return ( 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export const customGapColumn = () => { 61 | return ( 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/ButtonGroup/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from '@emotion/core'; 3 | 4 | export type ButtonGroupProps = { 5 | /** 버튼을 보여줄 방향 */ 6 | direction: 'row' | 'column'; 7 | /** 버튼을 우측에 보여줍니다. */ 8 | rightAlign?: boolean; 9 | /** 버튼과 버튼사이의 간격을 설정합니다. */ 10 | gap: number | string; 11 | /** 버튼 그룹에서 보여줄 버튼들 */ 12 | children: React.ReactNode; 13 | /* 스타일 커스터마이징 하고싶을 때 사용 */ 14 | className?: string; 15 | }; 16 | 17 | /** 18 | * 여러개의 `Button` 컴포넌트를 보여주고 싶거나, 버튼을 우측에 정렬하고 싶을 땐 `ButtonGroup` 컴포넌트를 사용하세요. 19 | */ 20 | const ButtonGroup = ({ 21 | direction, 22 | rightAlign, 23 | children, 24 | gap, 25 | className 26 | }: ButtonGroupProps) => { 27 | return ( 28 |
39 | {children} 40 |
41 | ); 42 | }; 43 | 44 | ButtonGroup.defaultProps = { 45 | direction: 'row', 46 | gap: '0.5rem' 47 | }; 48 | 49 | // direction 에 따라 margin-left 또는 margin-top 설정 50 | const gapStyle = (direction: 'row' | 'column', gap: number | string) => { 51 | const marginType = direction === 'row' ? 'marginLeft' : 'marginTop'; 52 | return css({ 53 | 'button + button': { 54 | [marginType]: gap 55 | } 56 | }); 57 | }; 58 | 59 | const rightAlignStyle = css` 60 | justify-content: flex-end; 61 | `; 62 | 63 | export default ButtonGroup; 64 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from './Dialog'; 3 | import { withKnobs, text, boolean } from '@storybook/addon-knobs'; 4 | 5 | export default { 6 | title: 'components|Dialog', 7 | component: Dialog, 8 | parameters: { 9 | docs: { 10 | inlineStories: false 11 | } 12 | }, 13 | decorators: [withKnobs] 14 | }; 15 | 16 | export const dialog = () => { 17 | const title = text('title', '결제 성공'); 18 | const description = text('description', '결제가 성공적으로 이루어졌습니다.'); 19 | const visible = boolean('visible', true); 20 | const confirmText = text('confirmText', '확인'); 21 | const cancelText = text('cancelText', '취소'); 22 | const cancellable = boolean('cancellable', false); 23 | 24 | return ( 25 | 33 | ); 34 | }; 35 | 36 | dialog.story = { 37 | name: 'Default' 38 | }; 39 | 40 | export const cancellable = () => { 41 | return ( 42 | 49 | ); 50 | }; 51 | 52 | export const customContent = () => { 53 | return ( 54 | 55 | Custom Content 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { Fragment } from 'react'; 3 | import { css, jsx } from '@emotion/core'; 4 | import ButtonGroup from '../ButtonGroup/ButtonGroup'; 5 | import Button from '../Button/Button'; 6 | import { useTransition, animated } from 'react-spring'; 7 | 8 | export type DialogProps = { 9 | visible: boolean; 10 | title?: string; 11 | description?: string; 12 | children?: React.ReactNode; 13 | hideButtons?: boolean; 14 | cancellable?: boolean; 15 | cancelText: string; 16 | confirmText: string; 17 | onCancel?: () => void; 18 | onConfirm?: () => void; 19 | }; 20 | 21 | const Dialog = ({ 22 | visible, 23 | title, 24 | description, 25 | hideButtons, 26 | cancellable, 27 | cancelText, 28 | confirmText, 29 | children, 30 | onCancel, 31 | onConfirm 32 | }: DialogProps) => { 33 | const fadeTransition = useTransition(visible, null, { 34 | from: { opacity: 0 }, 35 | enter: { opacity: 1 }, 36 | leave: { opacity: 0 } 37 | }); 38 | 39 | const slideUpTransition = useTransition(visible, null, { 40 | from: { 41 | transform: `translateY(200px) scale(0.8)`, 42 | opacity: 0 43 | }, 44 | enter: { 45 | transform: `translateY(0px) scale(1)`, 46 | opacity: 1 47 | }, 48 | leave: { 49 | transform: `translateY(200px) scale(0.8)`, 50 | opacity: 0 51 | }, 52 | config: { 53 | tension: 200, 54 | friction: 15 55 | } 56 | }); 57 | 58 | return ( 59 | 60 | {fadeTransition.map(({ item, key, props }) => 61 | item ? ( 62 | 67 | ) : null 68 | )} 69 | 70 | {slideUpTransition.map(({ item, key, props }) => 71 | item ? ( 72 | 77 |
78 | {title &&

{title}

} 79 | {description &&

{description}

} 80 | {children} 81 | {!hideButtons && ( 82 | 83 | {cancellable && ( 84 | 87 | )} 88 | 89 | 90 | )} 91 |
92 |
93 | ) : null 94 | )} 95 |
96 | ); 97 | }; 98 | 99 | Dialog.defaultProps = { 100 | cancelText: '취소', 101 | confirmText: '확인' 102 | }; 103 | 104 | const breakpoints = [576, 768, 992, 1200]; 105 | 106 | const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`); 107 | 108 | const fullscreen = css` 109 | position: fixed; 110 | top: 0; 111 | left: 0; 112 | width: 100%; 113 | height: 100%; 114 | `; 115 | 116 | const darkLayer = css` 117 | z-index: 10; 118 | background: rgba(0, 0, 0, 0.5); 119 | `; 120 | 121 | const whiteBoxWrapper = css` 122 | z-index: 15; 123 | display: flex; 124 | align-items: center; 125 | justify-content: center; 126 | `; 127 | 128 | const whiteBox = css` 129 | box-sizing: border-box; 130 | border-radius: 4px; 131 | width: 25rem; 132 | background: white; 133 | box-shadow: 0px 4px 8px 8px rgba(0, 0, 0, 0.05); 134 | padding: 2rem; 135 | 136 | h3 { 137 | font-size: 1.5rem; 138 | color: #343a40; 139 | margin-top: 0; 140 | margin-bottom: 1rem; 141 | } 142 | 143 | p { 144 | font-size: 1.125rem; 145 | margin: 0; 146 | color: #868e96; 147 | } 148 | 149 | ${mq[1]} { 150 | width: calc(100% - 2rem); 151 | padding: 1.5rem; 152 | h3 { 153 | font-size: 1.3125rem; 154 | } 155 | p { 156 | font-size: 0.875rem; 157 | } 158 | } 159 | `; 160 | 161 | export default Dialog; 162 | -------------------------------------------------------------------------------- /src/Icon/Icon.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx, css } from '@emotion/core'; 3 | import Icon, { iconTypes } from './Icon'; 4 | 5 | export default { 6 | component: Icon, 7 | title: 'components|Icon' 8 | }; 9 | 10 | export const icon = () => ; 11 | icon.story = { 12 | name: 'Default' 13 | }; 14 | 15 | export const customSize = () => ; 16 | 17 | export const customColor = () => ; 18 | 19 | export const customizedWithStyle = () => ( 20 | 21 | ); 22 | 23 | export const listOfIcons = () => { 24 | return ( 25 |
    26 | {iconTypes.map(icon => ( 27 |
  • 28 | 29 | {icon} 30 |
  • 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | const iconListStyle = css` 37 | list-style: none; 38 | display: flex; 39 | flex-wrap: wrap; 40 | li { 41 | box-sizing: border-box; 42 | width: 25%; 43 | padding: 1rem; 44 | display: flex; 45 | align-items: center; 46 | svg { 47 | margin-right: 1rem; 48 | } 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /src/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from '@emotion/core'; 3 | import * as icons from './svg'; 4 | 5 | type IconType = keyof typeof icons; 6 | export const iconTypes: IconType[] = Object.keys(icons) as any[]; // 스토리에서 불러오기 위함 7 | 8 | export type IconProps = { 9 | /** 사용 할 아이콘 타입 */ 10 | icon: IconType; 11 | /** 아이콘 색상 */ 12 | color?: string; 13 | /** 아이콘 크기 */ 14 | size?: string | number; 15 | className?: string; 16 | }; 17 | 18 | /** 아이콘을 보여주고 싶을 땐 `Icon` 컴포넌트를 사용하세요. 19 | * 20 | * 이 컴포넌트는 svg 형태로 아이콘을 보여주며, props 또는 스타일을 사용하여 아이콘의 색상과 크기를 정의 할 수 있습니다. 21 | * 22 | * 스타일로 모양새를 설정 할 때에는 `color`로 색상을 설정하고 `width`로 크기를 설정하세요. 23 | */ 24 | const Icon = ({ icon, color, size, className }: IconProps) => { 25 | const SVGIcon = icons[icon]; 26 | return ( 27 | 31 | ); 32 | }; 33 | 34 | export default Icon; 35 | -------------------------------------------------------------------------------- /src/Icon/svg/exit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Icon/svg/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Icon/svg/index.ts: -------------------------------------------------------------------------------- 1 | export { ReactComponent as exit } from './exit.svg'; 2 | export { ReactComponent as heart } from './heart.svg'; 3 | export { ReactComponent as pencil } from './pencil.svg'; 4 | -------------------------------------------------------------------------------- /src/Icon/svg/pencil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button/Button'; 2 | export { default as ButtonGroup } from './ButtonGroup/ButtonGroup'; 3 | export { default as Dialog } from './Dialog/Dialog'; 4 | export { default as Icon } from './Icon/Icon'; 5 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx'; 2 | 3 | declare module '*.svg' { 4 | import * as React from 'react'; 5 | 6 | export const ReactComponent: React.FunctionComponent>; 9 | 10 | const src: string; 11 | export default src; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "declaration": true, 15 | "declarationDir": "dist/types" 16 | }, 17 | "include": ["src"], 18 | "exclude": ["**/*.stories.tsx"] 19 | } 20 | --------------------------------------------------------------------------------