├── .babelrc.json ├── .eslintrc.json ├── .github └── workflows │ └── chromatic.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .storybook ├── main.ts └── preview.tsx ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── HangLogProvider.tsx ├── assets │ ├── index.d.ts │ └── svg │ │ ├── add-icon.svg │ │ ├── check-circle-icon.svg │ │ ├── checked-icon.svg │ │ ├── close-icon copy.svg │ │ ├── close-icon.svg │ │ ├── empty-star.svg │ │ ├── filled-star.svg │ │ ├── half-empty-left-star.svg │ │ ├── half-empty-right-star.svg │ │ ├── half-filled-left-star.svg │ │ ├── half-filled-right-star.svg │ │ ├── image-icon.svg │ │ ├── left-icon.svg │ │ ├── modal-sample.svg │ │ ├── right-icon.svg │ │ ├── search-icon.svg │ │ └── unchecked-icon.svg ├── components │ ├── Badge │ │ ├── Badge.style.ts │ │ └── Badge.tsx │ ├── Box │ │ ├── Box.style.ts │ │ └── Box.tsx │ ├── Button │ │ ├── Button.style.ts │ │ └── Button.tsx │ ├── Calendar │ │ ├── Calendar.style.ts │ │ ├── Calendar.tsx │ │ └── Day │ │ │ ├── Day.style.ts │ │ │ └── Day.tsx │ ├── Carousel │ │ └── Carousel.tsx │ ├── Center │ │ ├── Center.style.ts │ │ └── Center.tsx │ ├── Checkbox │ │ ├── Checkbox.style.ts │ │ └── Checkbox.tsx │ ├── DateRangePicker │ │ ├── DateRangePicker.style.ts │ │ └── DateRangePicker.tsx │ ├── Divider │ │ ├── Divider.style.ts │ │ └── Divider.tsx │ ├── Flex │ │ ├── Flex.style.ts │ │ └── Flex.tsx │ ├── FloatingButton │ │ ├── FloatingButton.style.ts │ │ └── FloatingButton.tsx │ ├── GeneralCarousel │ │ ├── Carousel.style.ts │ │ ├── Carousel.tsx │ │ ├── CarouselItem.tsx │ │ └── Dots.tsx │ ├── Heading │ │ ├── Heading.style.ts │ │ └── Heading.tsx │ ├── ImageCarousel │ │ ├── ImageCarousel.style.ts │ │ └── ImageCarousel.tsx │ ├── ImageUploadInput │ │ ├── ImageUploadInput.style.ts │ │ └── ImageUploadInput.tsx │ ├── Input │ │ ├── Input.style.ts │ │ └── Input.tsx │ ├── Label │ │ ├── Label.style.ts │ │ └── Label.tsx │ ├── Menu │ │ ├── Menu.style.ts │ │ └── Menu.tsx │ ├── MenuItem │ │ ├── MenuItem.style.ts │ │ └── MenuItem.tsx │ ├── MenuList │ │ ├── MenuList.style.ts │ │ └── MenuList.tsx │ ├── Modal │ │ ├── Modal.style.ts │ │ └── Modal.tsx │ ├── RadioButton │ │ ├── RadioButton.style.ts │ │ └── RadioButton.tsx │ ├── SVGCarousel │ │ ├── SVGCarousel.style.ts │ │ └── SVGCarousel.tsx │ ├── SVGCarouselModal │ │ ├── SVGCarouselModal.style.ts │ │ └── SVGCarouselModal.tsx │ ├── Select │ │ ├── Select.style.ts │ │ └── Select.tsx │ ├── Skeleton │ │ ├── Skeleton.style.ts │ │ └── Skeleton.tsx │ ├── Spinner │ │ ├── Spinner.style.ts │ │ └── Spinner.tsx │ ├── StarRatingInput │ │ ├── StarRatingInput.style.ts │ │ └── StarRatingInput.tsx │ ├── SupportingText │ │ ├── SupportingText.style.ts │ │ └── SupportingText.tsx │ ├── SwitchToggle │ │ ├── SwitchToggle.style.ts │ │ └── SwitchToggle.tsx │ ├── Tab │ │ ├── Tab.style.ts │ │ └── Tab.tsx │ ├── Tabs │ │ ├── Tabs.style.ts │ │ └── Tabs.tsx │ ├── Text │ │ ├── Text.style.ts │ │ └── Text.tsx │ ├── Textarea │ │ ├── Textarea.style.ts │ │ └── Textarea.tsx │ ├── Toast │ │ ├── Toast.style.ts │ │ └── Toast.tsx │ ├── ToastContainer │ │ ├── ToastContainer.style.ts │ │ └── ToastContainer.tsx │ ├── Toggle │ │ ├── Toggle.style.ts │ │ └── Toggle.tsx │ └── ToggleGroup │ │ ├── ToggleGroup.style.ts │ │ └── ToggleGroup.tsx ├── constants │ └── index.ts ├── hooks │ ├── useCalendar.ts │ ├── useCarousel.ts │ ├── useDateRangePicker.ts │ ├── useDebounce.ts │ ├── useImageCarousel.ts │ ├── useOverlay.ts │ ├── useSelect.ts │ └── useStarRatingInput.ts ├── index.tsx ├── stories │ ├── Badge.stories.tsx │ ├── Box.stories.tsx │ ├── Button.stories.tsx │ ├── Calendar.stories.tsx │ ├── Center.stories.tsx │ ├── Checkbox.stories.tsx │ ├── DateRangePicker.stories.tsx │ ├── Day.stories.tsx │ ├── Divider.stories.tsx │ ├── Flex.stories.tsx │ ├── FloatingButton.stories.tsx │ ├── GeneralCarousel.stories.tsx │ ├── Heading.stories.tsx │ ├── ImageCarousel.stories.tsx │ ├── ImageUploadInput.stories.tsx │ ├── Input.stories.tsx │ ├── Label.stories.ts │ ├── Menu.stories.tsx │ ├── Modal.stories.tsx │ ├── RadioButton.stories.tsx │ ├── SVGCarousel.stories.tsx │ ├── SVGCarouselModal.stories.tsx │ ├── Select.stories.tsx │ ├── Skeleton.stories.tsx │ ├── Spinner.stories.ts │ ├── StarRatingInput.stories.tsx │ ├── SupportingText.stories.ts │ ├── SwitchToggle.stories.tsx │ ├── Tabs.stories.tsx │ ├── Text.stories.tsx │ ├── Textarea.stories.tsx │ ├── Toast.stories.tsx │ ├── ToggleGroup.stories.tsx │ └── styles.ts ├── styles │ ├── GlobalStyle.ts │ ├── Theme.ts │ ├── animation │ │ └── index.ts │ └── style.d.ts ├── types │ ├── date.ts │ └── index.ts └── utils │ ├── date.ts │ └── number.ts ├── tsconfig.json └── webpack.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react", 14 | "@emotion/babel-preset-css-prop" 15 | ], 16 | "plugins": ["@emotion", "react-require"] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": ["react", "@typescript-eslint"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "airbnb", 11 | "airbnb/hooks", 12 | "plugin:react/jsx-runtime", 13 | "plugin:react-hooks/recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:jsx-a11y/recommended", 16 | "plugin:storybook/recommended", 17 | "plugin:prettier/recommended", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "import/order": "off", 22 | "class-methods-use-this": "off", 23 | "no-useless-constructor": "off", 24 | "no-use-before-define": "off", 25 | "no-shadow": "off", 26 | "@typescript-eslint/consistent-type-imports": [ 27 | "error", 28 | { 29 | "prefer": "type-imports" 30 | } 31 | ], 32 | "import/prefer-default-export": "off", 33 | "import/no-unresolved": "off", 34 | "import/extensions": "off", 35 | "import/no-extraneous-dependencies": "off", 36 | "react/jsx-filename-extension": [ 37 | 1, 38 | { 39 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 40 | } 41 | ], 42 | "react/display-name": "off", 43 | "react/prop-types": "off", 44 | "react/require-default-props": "off", 45 | "react/jsx-props-no-spreading": "off", 46 | "react/function-component-definition": [ 47 | "error", 48 | { 49 | "namedComponents": "arrow-function", 50 | "unnamedComponents": "arrow-function" 51 | } 52 | ], 53 | "react/no-unknown-property": ["error", { "ignore": ["css"] }], 54 | "react/state-in-constructor": "off" 55 | }, 56 | "overrides": [ 57 | { 58 | "files": ["**/*.stories.*"], 59 | "rules": { 60 | "import/no-anonymous-default-export": "off", 61 | "react-hooks/rules-of-hooks": "off" 62 | } 63 | } 64 | ], 65 | "parserOptions": { 66 | "ecmaVersion": "latest", 67 | "sourceType": "module" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | # Workflow name 2 | name: 'Chromatic Deployment' 3 | 4 | # Event for the workflow 5 | on: 6 | push: 7 | branches: ['main'] 8 | 9 | # List of jobs 10 | jobs: 11 | test: 12 | # Operating System 13 | runs-on: ubuntu-latest 14 | # Job steps 15 | steps: 16 | - uses: actions/checkout@v1 17 | - run: yarn 18 | #👇 Adds Chromatic as a step in the workflow 19 | - uses: chromaui/action@v1 20 | # Options required for Chromatic's GitHub Action 21 | with: 22 | #👇 Chromatic projectToken, see https://storybook.js.org/tutorials/intro-to-storybook/react/ko/deploy/ to obtain it 23 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | .DS_Store 5 | build-storybook.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | public/ 4 | tsconfig.json 5 | .storybook/ 6 | .babelrc.json 7 | .eslintrc.json 8 | .prettierrc 9 | webpack.config.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "printWidth": 100, 6 | "semi": true, 7 | "importOrder": [ 8 | "^@utils/(.*)$", 9 | "^@api/(.*)$", 10 | "^@hooks/(.*)$", 11 | "^@pages/(.*)$", 12 | "^@components/(.*)$", 13 | "^@styles/(.*)$", 14 | "^[./]" 15 | ], 16 | "importOrderSeparation": true, 17 | "importOrderSortSpecifiers": true 18 | } 19 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | import path from 'path'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-interactions', 10 | '@storybook/addon-a11y', 11 | ], 12 | framework: { 13 | name: '@storybook/react-webpack5', 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: true, 18 | }, 19 | webpackFinal: async (config) => { 20 | if (config.resolve) { 21 | config.resolve.alias = { 22 | ...config.resolve.alias, 23 | '@': path.resolve(__dirname, '../src'), 24 | '@components': path.resolve(__dirname, '../src/components'), 25 | '@type': path.resolve(__dirname, '../src/types'), 26 | '@hooks': path.resolve(__dirname, '../src/hooks'), 27 | '@styles': path.resolve(__dirname, '../src/styles'), 28 | '@constants': path.resolve(__dirname, '../src/constants'), 29 | '@assets': path.resolve(__dirname, '../src/assets'), 30 | '@stories': path.resolve(__dirname, '../src/stories'), 31 | '@utils': path.resolve(__dirname, '../src/utils'), 32 | }; 33 | } 34 | 35 | const imageRule = config.module?.rules?.find((rule) => { 36 | const test = (rule as { test: RegExp }).test; 37 | 38 | if (!test) return false; 39 | 40 | return test.test('.svg'); 41 | }) as { [key: string]: any }; 42 | 43 | imageRule.exclude = /\.svg$/; 44 | 45 | config.module?.rules?.push({ 46 | test: /\.svg$/, 47 | issuer: /\.(jsx|tsx)$/, 48 | use: ['@svgr/webpack'], 49 | }); 50 | config.module?.rules?.push({ 51 | test: /\.svg$/, 52 | issuer: /\.(js|ts)$/, 53 | use: ['url-loader'], 54 | }); 55 | 56 | return config; 57 | }, 58 | }; 59 | export default config; 60 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import { Global, ThemeProvider } from '@emotion/react'; 2 | import type { Preview } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import { GlobalStyle } from '../src/styles/GlobalStyle'; 6 | import { Theme } from '../src/styles/Theme'; 7 | 8 | const preview: Preview = { 9 | parameters: { 10 | actions: { argTypesRegex: '^on[A-Z].*' }, 11 | controls: { 12 | matchers: { 13 | color: /(background|color)$/i, 14 | date: /Date$/, 15 | }, 16 | }, 17 | }, 18 | }; 19 | 20 | export default preview; 21 | 22 | export const decorators = [ 23 | (Story) => ( 24 | 25 | 26 | 27 | 28 | ), 29 | ]; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 hang-log-design-system 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

Hang Log Design System

8 | 9 | A design system library for 행록(Hang Log), a place-based travel record service. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | $ npm install hang-log-design-system 15 | # or 16 | $ yarn add hang-log-design-system 17 | ``` 18 | 19 | ## Getting started 20 | 21 | To start using the components, first wrap your application in a provider provided by **hang-log-design-system** 22 | 23 | ```jsx 24 | import { HangLogProvider } from 'hang-log-design-system'; 25 | 26 | const App = ({ children }) => { 27 | return {children}; 28 | }; 29 | ``` 30 | 31 |
32 | 33 | After adding the provider, now you can start using components like this. 34 | 35 | ```jsx 36 | import { Button } from 'hang-log-design-system'; 37 | 38 | function App() { 39 | return ( 40 | 43 | ); 44 | } 45 | ``` 46 | 47 | ## Links 48 | 49 | - [Storybook](https://64ae1170f3ddc89ef85a4950-jugaezrbhx.chromatic.com/) 50 | - [Figma](https://www.figma.com/file/rJUqeL7LUnJjCPQNmQ3BZc/design-system?type=design&node-id=1%3A2854&mode=design&t=nVD5D8xFhO9Dkg6g-1) 51 | 52 | ## Contributors 53 | 54 | | | | | 55 | | :---------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | 56 | | [슬링키](https://github.com/dladncks1217) | [애슐리](https://github.com/ashleysyheo) | [헤다](https://github.com/Dahyeeee) | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hang-log-design-system", 3 | "version": "1.3.8", 4 | "description": "행록 디자인 시스템", 5 | "homepage": "https://github.com/hang-log-design-system/design-system", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "browser": "./browser/specific/main.js", 10 | "scripts": { 11 | "start": "webpack --mode development", 12 | "dev": "webpack serve --mode development --open --hot", 13 | "build": "webpack --mode production", 14 | "storybook": "storybook dev -p 6006", 15 | "build-storybook": "storybook build", 16 | "publish:npm": "rm -rf dist && mkdir dist && tsc && rm -rf ./dist/stories && cp -r ./src/assets ./dist && tsc-alias", 17 | "chromatic": "npx chromatic --project-token=chpt_9e90f3a3f28a532", 18 | "lint": "eslint src" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "ui", 23 | "design-system", 24 | "react-components", 25 | "uikit", 26 | "components", 27 | "emotion", 28 | "typescript", 29 | "library" 30 | ], 31 | "contributors": [ 32 | { 33 | "name": "Ashley Heo", 34 | "email": "ashleysyheo@gmail.com", 35 | "url": "https://github.com/ashleysyheo" 36 | }, 37 | { 38 | "name": "Woochan Lim", 39 | "email": "dlaxodud1217@gmail.com", 40 | "url": "https://github.com/dladncks1217" 41 | }, 42 | { 43 | "name": "Dahye Yun", 44 | "email": "06robin11@gmail.com", 45 | "url": "https://github.com/Dahyeeee" 46 | } 47 | ], 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/hang-log-design-system/design-system.git" 51 | }, 52 | "license": "MIT", 53 | "dependencies": { 54 | "@emotion/react": "^11.11.1", 55 | "@emotion/styled": "^11.11.0", 56 | "react": "^18.2.0", 57 | "react-dom": "^18.2.0" 58 | }, 59 | "devDependencies": { 60 | "@babel/preset-env": "^7.22.6", 61 | "@babel/preset-react": "^7.22.5", 62 | "@babel/preset-typescript": "^7.22.5", 63 | "@emotion/babel-plugin": "^11.11.0", 64 | "@emotion/babel-preset-css-prop": "^11.11.0", 65 | "@storybook/addon-a11y": "^7.1.1", 66 | "@storybook/addon-essentials": "^7.0.25", 67 | "@storybook/addon-interactions": "^7.0.25", 68 | "@storybook/addon-links": "^7.0.25", 69 | "@storybook/blocks": "^7.0.25", 70 | "@storybook/react": "^7.0.25", 71 | "@storybook/react-webpack5": "^7.0.25", 72 | "@storybook/testing-library": "^0.0.14-next.2", 73 | "@svgr/webpack": "^8.0.1", 74 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 75 | "@types/react": "^18.2.14", 76 | "@types/react-dom": "^18.2.6", 77 | "@typescript-eslint/eslint-plugin": "^6.2.0", 78 | "@typescript-eslint/parser": "^6.2.0", 79 | "babel-plugin-react-require": "^4.0.0", 80 | "chromatic": "^6.19.9", 81 | "eslint": "^8.44.0", 82 | "eslint-config-airbnb": "^19.0.4", 83 | "eslint-config-prettier": "^8.8.0", 84 | "eslint-plugin-import": "^2.27.5", 85 | "eslint-plugin-jsx-a11y": "^6.7.1", 86 | "eslint-plugin-prettier": "^4.2.1", 87 | "eslint-plugin-react": "^7.32.2", 88 | "eslint-plugin-react-hooks": "^4.6.0", 89 | "eslint-plugin-storybook": "^0.6.12", 90 | "html-webpack-plugin": "^5.5.3", 91 | "prettier": "^2.8.8", 92 | "storybook": "^7.0.25", 93 | "ts-loader": "^9.4.4", 94 | "tsc-alias": "^1.8.7", 95 | "typescript": "^5.1.6", 96 | "url-loader": "^4.1.1", 97 | "webpack": "^5.88.1", 98 | "webpack-cli": "^5.1.4", 99 | "webpack-dev-server": "^4.15.1" 100 | }, 101 | "bugs": { 102 | "url": "https://github.com/hang-log-design-system/design-system/issues" 103 | }, 104 | "readme": "ERROR: No README data found!", 105 | "_id": "hang-log-design-system@0.0.1" 106 | } 107 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 행록 디자인 시스템 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/HangLogProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Global, ThemeProvider } from '@emotion/react'; 2 | import type { PropsWithChildren } from 'react'; 3 | 4 | import ToastContainer from '@components/ToastContainer/ToastContainer'; 5 | 6 | import { GlobalStyle } from '@styles/GlobalStyle'; 7 | import { Theme } from '@styles/Theme'; 8 | 9 | type HangLogProviderProps = PropsWithChildren; 10 | 11 | const HangLogProvider = ({ children }: HangLogProviderProps) => { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default HangLogProvider; 22 | -------------------------------------------------------------------------------- /src/assets/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import type React from 'react'; 3 | 4 | const SVG: React.FC>; 5 | export default SVG; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/svg/add-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/check-circle-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/checked-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/svg/close-icon copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/close-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/empty-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/filled-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/half-empty-left-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/half-empty-right-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/half-filled-left-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/half-filled-right-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/image-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/left-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/right-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/search-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/unchecked-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { BadgeProps } from '@components/Badge/Badge'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getVariantStyling = (variant: Required['variant']) => { 8 | const style = { 9 | default: css({ 10 | backgroundColor: Theme.color.blue200, 11 | 12 | color: Theme.color.blue700, 13 | }), 14 | primary: css({ 15 | backgroundColor: Theme.color.blue500, 16 | 17 | color: Theme.color.white, 18 | }), 19 | outline: css({ 20 | backgroundColor: Theme.color.white, 21 | boxShadow: `inset 0 0 0 1px ${Theme.color.blue500}`, 22 | 23 | color: Theme.color.blue500, 24 | }), 25 | }; 26 | 27 | return style[variant]; 28 | }; 29 | 30 | export const badgeStyling = css({ 31 | padding: `4px 10px`, 32 | borderRadius: Theme.borderRadius.small, 33 | 34 | fontSize: Theme.text.xSmall.fontSize, 35 | lineHeight: Theme.text.xSmall.lineHeight, 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { badgeStyling, getVariantStyling } from '@components/Badge/Badge.style'; 4 | 5 | export interface BadgeProps extends ComponentPropsWithoutRef<'div'> { 6 | /** 7 | * Badge의 비주얼 스타일 8 | * 9 | * @default 'default' 10 | */ 11 | variant?: 'default' | 'primary' | 'outline'; 12 | } 13 | 14 | const Badge = ({ variant = 'default', children, ...attributes }: BadgeProps) => { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | export default Badge; 23 | -------------------------------------------------------------------------------- /src/components/Box/Box.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export interface BoxStylingProps { 6 | width?: string; 7 | height?: string; 8 | margin?: string; 9 | marginRight?: string; 10 | marginTop?: string; 11 | marginLeft?: string; 12 | marginBottom?: string; 13 | padding?: string; 14 | paddingTop?: string; 15 | paddingRight?: string; 16 | paddingBottom?: string; 17 | paddingLeft?: string; 18 | border?: string; 19 | borderRadius?: string; 20 | borderColor?: string; 21 | borderTop?: string; 22 | borderRight?: string; 23 | borderBottom?: string; 24 | borderLeft?: string; 25 | backgroundColor?: string; 26 | color?: string; 27 | position?: 'static' | 'absolute' | 'relative' | 'fixed' | 'inherit'; 28 | } 29 | 30 | export const getBoxStyling = ({ 31 | width = '', 32 | height = '', 33 | margin = '', 34 | marginRight = '', 35 | marginTop = '', 36 | marginLeft = '', 37 | marginBottom = '', 38 | padding = '', 39 | paddingTop = '', 40 | paddingRight = '', 41 | paddingBottom = '', 42 | paddingLeft = '', 43 | border = '', 44 | borderRadius = '', 45 | borderColor = `${Theme.color.gray200}`, 46 | borderTop = '', 47 | borderRight = '', 48 | borderBottom = '', 49 | borderLeft = '', 50 | backgroundColor = '', 51 | color = '', 52 | position = 'static', 53 | }: BoxStylingProps) => { 54 | return css({ 55 | width, 56 | height, 57 | margin, 58 | marginRight, 59 | marginTop, 60 | marginLeft, 61 | marginBottom, 62 | padding, 63 | paddingTop, 64 | paddingRight, 65 | paddingBottom, 66 | paddingLeft, 67 | border, 68 | borderRadius, 69 | borderColor, 70 | borderTop, 71 | borderRight, 72 | borderBottom, 73 | borderLeft, 74 | backgroundColor, 75 | color, 76 | position, 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/Box/Box.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef, ElementType } from 'react'; 2 | 3 | import type { BoxStylingProps } from '@components/Box/Box.style'; 4 | import { getBoxStyling } from '@components/Box/Box.style'; 5 | 6 | export interface BoxProps extends ComponentPropsWithoutRef<'div'> { 7 | /** 8 | * Box 컴포넌트가 사용할 HTML 태그 9 | * 10 | * @default 'div' 11 | */ 12 | tag?: ElementType; 13 | /** Box 컴포넌트 스타일 옵션 */ 14 | styles?: BoxStylingProps; 15 | } 16 | 17 | const Box = ({ tag = 'div', styles = {}, children, ...attributes }: BoxProps) => { 18 | const Tag = tag; 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default Box; 28 | -------------------------------------------------------------------------------- /src/components/Button/Button.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | import type { ButtonProps } from './Button'; 6 | 7 | export const getVariantStyling = (variant: Required['variant']) => { 8 | const style = { 9 | primary: css({ 10 | backgroundColor: Theme.color.blue500, 11 | 12 | color: Theme.color.white, 13 | 14 | '&:hover:enabled': { 15 | backgroundColor: Theme.color.blue600, 16 | }, 17 | 18 | '&:focus': { 19 | boxShadow: `0 0 0 3px ${Theme.color.blue600}`, 20 | }, 21 | }), 22 | secondary: css({ 23 | backgroundColor: Theme.color.blue100, 24 | 25 | color: Theme.color.blue600, 26 | 27 | '&:hover:enabled': { 28 | backgroundColor: Theme.color.blue200, 29 | }, 30 | 31 | '&:focus': { 32 | boxShadow: `0 0 0 3px ${Theme.color.blue200}`, 33 | }, 34 | }), 35 | default: css({ 36 | backgroundColor: Theme.color.gray100, 37 | 38 | color: Theme.color.gray800, 39 | 40 | '&:hover:enabled': { 41 | backgroundColor: `${Theme.color.gray200}`, 42 | }, 43 | 44 | '&:focus': { 45 | boxShadow: `0 0 0 3px ${Theme.color.gray200}`, 46 | }, 47 | }), 48 | outline: css({ 49 | backgroundColor: Theme.color.white, 50 | 51 | color: Theme.color.gray800, 52 | boxShadow: `inset 0 0 0 1px ${Theme.color.gray200}`, 53 | 54 | '&:hover:enabled': { 55 | backgroundColor: Theme.color.gray200, 56 | }, 57 | 58 | '&:focus': { 59 | boxShadow: `0 0 0 3px ${Theme.color.gray300}`, 60 | }, 61 | }), 62 | 63 | text: css({ 64 | backgroundColor: Theme.color.white, 65 | 66 | color: Theme.color.gray800, 67 | 68 | '&:hover:enabled': { 69 | backgroundColor: Theme.color.gray100, 70 | }, 71 | 72 | '&:focus': { 73 | boxShadow: `0 0 0 3px ${Theme.color.gray100}`, 74 | }, 75 | }), 76 | danger: css({ 77 | backgroundColor: Theme.color.red200, 78 | 79 | color: Theme.color.white, 80 | 81 | '&:hover:enabled': { 82 | backgroundColor: Theme.color.red300, 83 | }, 84 | 85 | '&:focus': { 86 | boxShadow: `0 0 0 3px ${Theme.color.red300}`, 87 | }, 88 | }), 89 | }; 90 | 91 | return style[variant]; 92 | }; 93 | 94 | export const getSizeStyling = (size: Required['size']) => { 95 | const style = { 96 | large: css({ 97 | padding: '14px 16px', 98 | 99 | fontSize: Theme.text.medium.fontSize, 100 | lineHeight: Theme.text.medium.lineHeight, 101 | }), 102 | medium: css({ 103 | padding: '12px 16px', 104 | 105 | fontSize: Theme.text.medium.fontSize, 106 | lineHeight: Theme.text.medium.lineHeight, 107 | }), 108 | small: css({ 109 | padding: '8px 12px', 110 | 111 | fontSize: Theme.text.small.fontSize, 112 | lineHeight: Theme.text.small.lineHeight, 113 | }), 114 | }; 115 | 116 | return style[size]; 117 | }; 118 | 119 | export const buttonStyling = css({ 120 | display: 'flex', 121 | justifyContent: 'center', 122 | alignItems: 'center', 123 | 124 | border: 'none', 125 | borderRadius: `${Theme.borderRadius.small}`, 126 | outline: `0 solid ${Theme.color.white}`, 127 | 128 | backgroundColor: Theme.color.white, 129 | 130 | fontWeight: 600, 131 | 132 | transition: 'all .2s ease-in', 133 | 134 | cursor: 'pointer', 135 | 136 | '&:focus': { 137 | outlineWidth: '1px', 138 | }, 139 | 140 | '&:disabled': { 141 | opacity: '.4', 142 | }, 143 | }); 144 | -------------------------------------------------------------------------------- /src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 3 | import { forwardRef } from 'react'; 4 | 5 | import { buttonStyling, getSizeStyling, getVariantStyling } from '@components/Button/Button.style'; 6 | 7 | export interface ButtonProps extends ComponentPropsWithRef<'button'> { 8 | size?: Extract; 9 | variant?: 'primary' | 'secondary' | 'default' | 'outline' | 'text' | 'danger'; 10 | } 11 | 12 | const Button = ( 13 | { size = 'medium', variant = 'default', children, ...attributes }: ButtonProps, 14 | ref: ForwardedRef 15 | ) => { 16 | return ( 17 | // eslint-disable-next-line react/button-has-type 18 | 25 | ); 26 | }; 27 | 28 | export default forwardRef(Button); 29 | -------------------------------------------------------------------------------- /src/components/Calendar/Calendar.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const containerStyling = css({ 6 | display: 'inline-block', 7 | }); 8 | 9 | export const headerStyling = css({ 10 | marginBottom: Theme.spacer.spacing3, 11 | 12 | textAlign: 'center', 13 | 14 | '& > h6': { 15 | fontWeight: 600, 16 | }, 17 | }); 18 | 19 | export const dayContainerStyling = css({ 20 | display: 'grid', 21 | gridTemplateColumns: 'repeat(7, 40px)', 22 | gridAutoRows: '40px', 23 | columnGap: '2px', 24 | rowGap: '2px', 25 | }); 26 | 27 | export const dayOfWeekContainerStyling = css({ 28 | marginBottom: Theme.spacer.spacing2, 29 | 30 | '& div': { 31 | color: Theme.color.gray600, 32 | 33 | cursor: 'default', 34 | 35 | '& span:hover': { 36 | backgroundColor: Theme.color.white, 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/Calendar/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import { DAYS_OF_WEEK } from '@constants/index'; 2 | import type { SelectedDateRange, YearMonth } from '@type/date'; 3 | import { useMemo } from 'react'; 4 | 5 | import { getDayBoxSize, getDayInfo } from '@utils/date'; 6 | 7 | import { 8 | containerStyling, 9 | dayContainerStyling, 10 | dayOfWeekContainerStyling, 11 | headerStyling, 12 | } from '@components/Calendar/Calendar.style'; 13 | import Day from '@components/Calendar/Day/Day'; 14 | import Heading from '@components/Heading/Heading'; 15 | 16 | export interface CalendarProps { 17 | /** 현재 Date */ 18 | currentDate: Date; 19 | /** 현재 년월 정보 */ 20 | yearMonthData: YearMonth; 21 | /** 현재 선택된 날짜 범위 */ 22 | dateRange?: SelectedDateRange; 23 | /** 오늘 이후 날짜를 막을 것인지에 대한 여부 */ 24 | isFutureDaysRestricted?: boolean; 25 | /** 특정 범위를 벗어나는 날짜에 대해서 선택 불가능할지에 대한 여부 */ 26 | hasRangeRestriction?: boolean; 27 | /** 최대로 선택할 수 있는 날짜 범위 */ 28 | maxDateRange?: number; 29 | /** 현재 선택된 날짜 */ 30 | selectedDate?: number; 31 | /** 특정 날짜를 선택했을 때 실행할 함수 */ 32 | onDateClick?: CallableFunction; 33 | } 34 | 35 | const Calendar = ({ 36 | currentDate, 37 | yearMonthData, 38 | dateRange, 39 | isFutureDaysRestricted, 40 | hasRangeRestriction, 41 | maxDateRange, 42 | selectedDate, 43 | onDateClick, 44 | }: CalendarProps) => { 45 | const dayBoxSize = useMemo(() => getDayBoxSize(yearMonthData), [yearMonthData]); 46 | 47 | return ( 48 |
55 |
61 | 62 | {yearMonthData.year}.{yearMonthData.month} 63 | 64 |
65 |
66 | {DAYS_OF_WEEK.map((day) => ( 67 | 68 | ))} 69 |
70 |
71 | {Array.from({ length: dayBoxSize }, (_, index) => { 72 | const { date, isDate, dateString, isToday, isSelected, isInRange, isRestricted } = 73 | getDayInfo({ 74 | index, 75 | yearMonthData, 76 | currentDate, 77 | dateRange, 78 | maxDateRange, 79 | isFutureDaysRestricted, 80 | hasRangeRestriction, 81 | selectedDate, 82 | }); 83 | 84 | return isDate ? ( 85 | 96 | ) : ( 97 | 98 | ); 99 | })} 100 |
101 |
102 | ); 103 | }; 104 | 105 | export default Calendar; 106 | -------------------------------------------------------------------------------- /src/components/Calendar/Day/Day.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const dayContainerStyling = css({ 6 | minWidth: '40px', 7 | width: '40px', 8 | height: '40px', 9 | }); 10 | 11 | export const getDayStyling = (isClickable: boolean) => { 12 | return css({ 13 | display: 'flex', 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | 17 | width: '40px', 18 | height: '40px', 19 | backgroundColor: Theme.color.white, 20 | 21 | fontSize: Theme.text.small.fontSize, 22 | fontWeight: '500', 23 | 24 | cursor: isClickable ? 'pointer' : 'default', 25 | 26 | '&:hover': { 27 | backgroundColor: Theme.color.gray100, 28 | }, 29 | }); 30 | }; 31 | 32 | export const getTodayStyling = (isToday: boolean) => { 33 | return ( 34 | isToday && 35 | css({ 36 | backgroundColor: Theme.color.blue300, 37 | 38 | color: Theme.color.blue800, 39 | 40 | '&:hover': { 41 | backgroundColor: Theme.color.blue300, 42 | }, 43 | }) 44 | ); 45 | }; 46 | 47 | export const getDayInRangeStyling = (isInRange: boolean) => { 48 | return ( 49 | isInRange && 50 | css({ 51 | backgroundColor: Theme.color.blue100, 52 | 53 | color: Theme.color.blue600, 54 | 55 | '&:hover': { 56 | backgroundColor: Theme.color.blue100, 57 | }, 58 | }) 59 | ); 60 | }; 61 | 62 | export const getSelectedDayStyling = (isSelected: boolean) => { 63 | return ( 64 | isSelected && 65 | css({ 66 | backgroundColor: Theme.color.blue500, 67 | 68 | color: Theme.color.white, 69 | 70 | '&:hover': { 71 | backgroundColor: Theme.color.blue500, 72 | }, 73 | }) 74 | ); 75 | }; 76 | 77 | export const getDisabledDayStyling = (isDisabled: boolean) => { 78 | return ( 79 | isDisabled && 80 | css({ 81 | backgroundColor: Theme.color.white, 82 | 83 | color: Theme.color.gray500, 84 | 85 | pointerEvents: 'none', 86 | 87 | '&:hover': { 88 | backgroundColor: Theme.color.white, 89 | }, 90 | }) 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/Calendar/Day/Day.tsx: -------------------------------------------------------------------------------- 1 | import type { KeyboardEvent } from 'react'; 2 | 3 | import { 4 | dayContainerStyling, 5 | getDayInRangeStyling, 6 | getDayStyling, 7 | getDisabledDayStyling, 8 | getSelectedDayStyling, 9 | getTodayStyling, 10 | } from '@components/Calendar/Day/Day.style'; 11 | 12 | export interface DayProps { 13 | /** 년 */ 14 | year?: number | string; 15 | /** 월 */ 16 | month?: number | string; 17 | /** 날짜 또는 요일 */ 18 | day?: number | string; 19 | /** 날짜가 오늘인지에 대한 여부 */ 20 | isToday?: boolean; 21 | /** 날짜가 선택되었는지에 대한 여부 */ 22 | isSelected?: boolean; 23 | /** 날짜가 선택된 날짜 범위 안에 있는에 대한 여부 */ 24 | isInRange?: boolean; 25 | /** 날짜 선택이 불가능한지에 대한 여부 */ 26 | isDisabled?: boolean; 27 | /** 날짜를 클릭하면 발생할 이벤트 */ 28 | onClick?: () => void; 29 | } 30 | 31 | const Day = ({ 32 | year, 33 | month, 34 | day, 35 | isToday = false, 36 | isSelected = false, 37 | isInRange = false, 38 | isDisabled = false, 39 | onClick, 40 | }: DayProps) => { 41 | const handleOptionKeyPress = (event: KeyboardEvent) => { 42 | if (event.key === 'Enter') { 43 | onClick?.(); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 | {day && ( 50 | 64 | {day} 65 | 66 | )} 67 |
68 | ); 69 | }; 70 | 71 | export default Day; 72 | -------------------------------------------------------------------------------- /src/components/Carousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | import LeftIcon from '@assets/svg/left-icon.svg'; 3 | import RightIcon from '@assets/svg/right-icon.svg'; 4 | 5 | import { useImageCarousel } from '@hooks/useImageCarousel'; 6 | 7 | import Box from '@components/Box/Box'; 8 | import { 9 | dotStyling, 10 | getButtonContainerStyling, 11 | getContainerStyling, 12 | getDotsWrapperStyling, 13 | getSliderContainerStyling, 14 | leftButtonStyling, 15 | rightButtonStyling, 16 | sliderWrapperStyling, 17 | } from '@components/ImageCarousel/ImageCarousel.style'; 18 | 19 | export interface ImageCarouselProps { 20 | width: number; 21 | height: number; 22 | images: string[]; 23 | isDraggable?: boolean; 24 | showArrows?: boolean; 25 | showDots?: boolean; 26 | showNavigationOnHover?: boolean; 27 | children: JSX.Element | JSX.Element[]; 28 | } 29 | 30 | const ImageCarousel = ({ 31 | width, 32 | height, 33 | images, 34 | isDraggable = false, 35 | showArrows = false, 36 | showDots = false, 37 | showNavigationOnHover = false, 38 | children, 39 | }: ImageCarouselProps) => { 40 | const { 41 | sliderRef, 42 | animate, 43 | currentPosition, 44 | translateX, 45 | handleSliderNavigationClick, 46 | handleSliderNavigationEnterKeyPress, 47 | handlerSliderMoueDown, 48 | handleSliderTouchStart, 49 | handleSliderTransitionEnd, 50 | } = useImageCarousel(width, images.length); 51 | 52 | return ( 53 | 59 | 60 |
67 | {children} 68 |
69 |
70 | {showArrows && images.length !== 1 && ( 71 |
72 | 80 | 88 |
89 | )} 90 | {showDots && ( 91 |
92 | {Array.from({ length: images.length }, (_, index) => ( 93 | 102 | ))} 103 |
104 | )} 105 |
106 | ); 107 | }; 108 | 109 | export default ImageCarousel; 110 | -------------------------------------------------------------------------------- /src/components/Center/Center.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const getCenterStyling = css({ 4 | display: 'flex', 5 | justifyContent: 'center', 6 | alignItems: 'center', 7 | 8 | width: '100%', 9 | height: '100%', 10 | 11 | textAlign: 'center', 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/Center/Center.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef, ElementType } from 'react'; 2 | 3 | import { getCenterStyling } from '@components/Center/Center.style'; 4 | 5 | export interface CenterProps extends ComponentPropsWithoutRef<'div'> { 6 | /** 7 | * Center 컴포넌트가 사용할 HTML 태그 8 | * 9 | * @default 'div' 10 | */ 11 | tag?: ElementType; 12 | } 13 | 14 | const Center = ({ tag = 'div', children, ...attributes }: CenterProps) => { 15 | const Tag = tag; 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default Center; 25 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const checkboxStyling = css({ 4 | display: 'flex', 5 | gap: '12px', 6 | alignItems: 'center', 7 | 8 | '& > svg': { 9 | width: '28px', 10 | height: '28px', 11 | }, 12 | }); 13 | 14 | export const inputStyling = css({ 15 | display: 'none', 16 | padding: 0, 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import CheckedIcon from '@assets/svg/checked-icon.svg'; 2 | import UncheckedIcon from '@assets/svg/unchecked-icon.svg'; 3 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 4 | import { forwardRef, useCallback, useState } from 'react'; 5 | 6 | import { checkboxStyling, inputStyling } from '@components/Checkbox/Checkbox.style'; 7 | 8 | export interface CheckboxProps extends ComponentPropsWithRef<'input'> { 9 | label?: string; 10 | isChecked?: boolean; 11 | } 12 | 13 | const Checkbox = ( 14 | { id, label = '', isChecked = false, ...attributes }: CheckboxProps, 15 | ref: ForwardedRef 16 | ) => { 17 | const [checked, setChecked] = useState(isChecked); 18 | 19 | const handleChecked = useCallback(() => { 20 | setChecked(!checked); 21 | }, [checked]); 22 | 23 | return ( 24 | 40 | ); 41 | }; 42 | 43 | export default forwardRef(Checkbox); 44 | -------------------------------------------------------------------------------- /src/components/DateRangePicker/DateRangePicker.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const containerStyling = css({ 6 | position: 'relative', 7 | display: 'inline-flex', 8 | gap: Theme.spacer.spacing3, 9 | alignItems: 'flex-start', 10 | 11 | padding: Theme.spacer.spacing3, 12 | 13 | '@media screen and (max-width: 632px)': { 14 | flexWrap: 'wrap', 15 | }, 16 | 17 | '& > button': { 18 | position: 'absolute', 19 | top: '21px', 20 | 21 | backgroundColor: 'white', 22 | border: 'none', 23 | outline: 'white', 24 | 25 | cursor: 'pointer', 26 | 27 | '& svg': { 28 | width: '16px', 29 | height: '16px', 30 | 31 | '& path': { 32 | stroke: Theme.color.gray600, 33 | }, 34 | 35 | '&:hover path': { 36 | stroke: Theme.color.black, 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | export const previousButtonStyling = css({ 43 | '@media screen and (max-width: 632px)': { 44 | left: Theme.spacer.spacing3, 45 | }, 46 | }); 47 | 48 | export const nextButtonStyling = css({ 49 | right: Theme.spacer.spacing3, 50 | 51 | '@media screen and (max-width: 632px)': { 52 | left: '292px', 53 | right: 'unset', 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/DateRangePicker/DateRangePicker.tsx: -------------------------------------------------------------------------------- 1 | import LeftIcon from '@assets/svg/left-icon.svg'; 2 | import RightIcon from '@assets/svg/right-icon.svg'; 3 | import { CALENDAR_MONTH_CHANGE, DEFAULT_MAX_DATE_RANGE } from '@constants/index'; 4 | import type { SelectedDateRange, YearMonth } from '@type/date'; 5 | 6 | import { formatDate } from '@utils/date'; 7 | 8 | import { useDateRangePicker } from '@hooks/useDateRangePicker'; 9 | 10 | import Calendar from '@components/Calendar/Calendar'; 11 | import { 12 | containerStyling, 13 | nextButtonStyling, 14 | } from '@components/DateRangePicker/DateRangePicker.style'; 15 | 16 | export interface DateRangePickerProps { 17 | /** 오늘 이후 날짜를 막을 것인지에 대한 여부 */ 18 | isFutureDaysRestricted?: boolean; 19 | /** 특정 범위를 벗어나는 날짜에 대해서 선택 불가능할지에 대한 여부 */ 20 | hasRangeRestriction?: boolean; 21 | /** 최대로 선택할 수 있는 날짜 범위 */ 22 | maxDateRange?: number; 23 | /** 현재 선택된 날짜 범위 */ 24 | initialSelectedDateRange?: SelectedDateRange; 25 | /** 날짜를 선택했을 때 실행할 함수 */ 26 | onDateSelect?: CallableFunction; 27 | } 28 | 29 | const DateRangePicker = ({ 30 | isFutureDaysRestricted = false, 31 | hasRangeRestriction = false, 32 | maxDateRange = DEFAULT_MAX_DATE_RANGE, 33 | initialSelectedDateRange, 34 | onDateSelect, 35 | }: DateRangePickerProps) => { 36 | const { currentDate, calendarData, handleMonthChange, selectedDateRange, handleDateSelect } = 37 | useDateRangePicker(initialSelectedDateRange); 38 | 39 | const handleDateClick = (date: number, yearMonth: YearMonth) => () => { 40 | const clickedDate = formatDate(yearMonth.year, yearMonth.month, date); 41 | handleDateSelect(clickedDate, onDateSelect); 42 | }; 43 | 44 | return ( 45 |
46 | 53 | 62 | 71 | 79 |
80 | ); 81 | }; 82 | 83 | export default DateRangePicker; 84 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { DividerProps } from '@components/Divider/Divider'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getDirectionStyling = ( 8 | direction: Required['direction'], 9 | length: Required['length'] 10 | ) => { 11 | const style = { 12 | horizontal: css({ 13 | borderBottom: `1px solid ${Theme.color.gray200}`, 14 | width: length, 15 | }), 16 | vertical: css({ 17 | borderLeft: `1px solid ${Theme.color.gray200}`, 18 | height: length, 19 | }), 20 | }; 21 | 22 | return style[direction]; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { getDirectionStyling } from '@components/Divider/Divider.style'; 4 | 5 | export interface DividerProps extends ComponentPropsWithoutRef<'div'> { 6 | length?: string; 7 | direction?: 'horizontal' | 'vertical'; 8 | } 9 | 10 | const Divider = ({ length = '100%', direction = 'horizontal', ...attributes }: DividerProps) => { 11 | return
; 12 | }; 13 | 14 | export default Divider; 15 | -------------------------------------------------------------------------------- /src/components/Flex/Flex.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export interface FlexStylingProps { 4 | direction?: 'row' | 'column'; 5 | wrap?: 'nowrap' | 'wrap' | 'wrap-reverse'; 6 | basis?: 'auto' | '0' | '200px'; 7 | grow?: string; 8 | shrink?: string; 9 | align?: 10 | | 'normal' 11 | | 'stretch' 12 | | 'center' 13 | | 'start' 14 | | 'end' 15 | | 'flex-start' 16 | | 'flex-end' 17 | | 'self-start' 18 | | 'self-end' 19 | | 'baseline' 20 | | 'inherit' 21 | | 'initial' 22 | | 'unset'; 23 | justify?: 24 | | 'center' 25 | | 'start' 26 | | 'flex-start' 27 | | 'end' 28 | | 'flex-end' 29 | | 'left' 30 | | 'right' 31 | | 'normal' 32 | | 'space-between' 33 | | 'space-around' 34 | | 'space-evenly' 35 | | 'stretch' 36 | | 'inherit' 37 | | 'initial' 38 | | 'revert' 39 | | 'unset'; 40 | gap?: string; 41 | margin?: string; 42 | marginRight?: string; 43 | marginTop?: string; 44 | marginLeft?: string; 45 | marginBottom?: string; 46 | padding?: string; 47 | paddingTop?: string; 48 | paddingRight?: string; 49 | paddingBottom?: string; 50 | paddingLeft?: string; 51 | border?: string; 52 | borderRadius?: string; 53 | borderColor?: string; 54 | borderTop?: string; 55 | borderRight?: string; 56 | borderBottom?: string; 57 | borderLeft?: string; 58 | width?: string; 59 | height?: string; 60 | position?: 'static' | 'absolute' | 'relative' | 'fixed' | 'inherit'; 61 | } 62 | 63 | export const getFlexStyling = ({ 64 | direction = 'row', 65 | wrap = 'nowrap', 66 | basis = 'auto', 67 | grow = '1', 68 | shrink = '0', 69 | align = 'flex-start', 70 | justify = 'flex-start', 71 | gap = '0px', 72 | margin = '0', 73 | marginRight = '', 74 | marginTop = '', 75 | marginLeft = '', 76 | marginBottom = '', 77 | padding = '', 78 | paddingTop = '', 79 | paddingRight = '', 80 | paddingBottom = '', 81 | paddingLeft = '', 82 | border = '', 83 | borderRadius = '', 84 | borderColor = '', 85 | borderTop = '', 86 | borderRight = '', 87 | borderBottom = '', 88 | borderLeft = '', 89 | width = '', 90 | height = '', 91 | position = 'static', 92 | }: FlexStylingProps) => { 93 | return css({ 94 | display: 'flex', 95 | flexDirection: direction, 96 | flexWrap: wrap, 97 | flexBasis: basis, 98 | grow, 99 | flexShrink: shrink, 100 | alignItems: align, 101 | justifyContent: justify, 102 | gap, 103 | margin, 104 | marginRight, 105 | marginTop, 106 | marginLeft, 107 | marginBottom, 108 | padding, 109 | paddingTop, 110 | paddingRight, 111 | paddingBottom, 112 | paddingLeft, 113 | border, 114 | borderRadius, 115 | borderColor, 116 | borderTop, 117 | borderRight, 118 | borderBottom, 119 | borderLeft, 120 | width, 121 | height, 122 | position, 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef, ElementType } from 'react'; 2 | 3 | import type { FlexStylingProps } from '@components/Flex/Flex.style'; 4 | import { getFlexStyling } from '@components/Flex/Flex.style'; 5 | 6 | export interface FlexProps extends ComponentPropsWithoutRef<'div'> { 7 | /** 8 | * Flex 컴포넌트가 사용할 HTML 태그 9 | * 10 | * @default 'div' 11 | */ 12 | tag?: ElementType; 13 | /** Flex 컴포넌트 스타일 옵션 */ 14 | styles?: FlexStylingProps; 15 | } 16 | 17 | const Flex = ({ tag = 'div', styles = {}, children, ...attributes }: FlexProps) => { 18 | const Tag = tag; 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default Flex; 28 | -------------------------------------------------------------------------------- /src/components/FloatingButton/FloatingButton.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { FloatingButtonProps } from '@components/FloatingButton/FloatingButton'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getSizeStyling = (size: Required['size']) => { 8 | const style = { 9 | medium: css({ 10 | width: '64px', 11 | height: '64px', 12 | }), 13 | 14 | small: css({ 15 | width: '32px', 16 | height: '32px', 17 | }), 18 | }; 19 | 20 | return style[size]; 21 | }; 22 | 23 | export const getIconSizeStyling = (size: Required['size']) => { 24 | const style = { 25 | medium: css({ 26 | width: '24px', 27 | height: '24px', 28 | }), 29 | small: css({ 30 | width: '16px', 31 | height: '16px', 32 | 33 | path: { 34 | strokeWidth: '1', 35 | }, 36 | }), 37 | }; 38 | 39 | return style[size]; 40 | }; 41 | 42 | export const getIconVariantStyling = (variant: Required['variant']) => { 43 | const style = { 44 | primary: css({ 45 | path: { 46 | stroke: Theme.color.white, 47 | }, 48 | }), 49 | default: css({ 50 | path: { 51 | stroke: Theme.color.black, 52 | }, 53 | }), 54 | }; 55 | 56 | return style[variant]; 57 | }; 58 | 59 | export const floatingButtonStyling = css({ 60 | display: 'flex', 61 | justifyContent: 'center', 62 | alignItems: 'center', 63 | 64 | border: 'none', 65 | borderRadius: '50%', 66 | outline: `0 solid ${Theme.color.white}`, 67 | 68 | boxShadow: Theme.boxShadow.shadow5, 69 | 70 | transition: 'all .2s ease-in', 71 | 72 | cursor: 'pointer', 73 | 74 | '&:focus': { 75 | outlineWidth: '1px', 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/FloatingButton/FloatingButton.tsx: -------------------------------------------------------------------------------- 1 | import AddIcon from '@assets/svg/add-icon.svg'; 2 | import type { Size } from '@type/index'; 3 | import type { ComponentPropsWithoutRef } from 'react'; 4 | 5 | import { getVariantStyling } from '@components/Button/Button.style'; 6 | import { 7 | floatingButtonStyling, 8 | getIconSizeStyling, 9 | getIconVariantStyling, 10 | getSizeStyling, 11 | } from '@components/FloatingButton/FloatingButton.style'; 12 | 13 | export interface FloatingButtonProps extends ComponentPropsWithoutRef<'button'> { 14 | /** 15 | * FloatingButton의 시이즈 16 | * 17 | * @default 'medium' 18 | */ 19 | size?: Extract; 20 | /** 21 | * FloatingButton의 색상 22 | * 23 | * @default 'primary' 24 | */ 25 | variant?: 'primary' | 'default'; 26 | } 27 | 28 | const FloatingButton = ({ 29 | size = 'medium', 30 | variant = 'primary', 31 | ...attributes 32 | }: FloatingButtonProps) => { 33 | return ( 34 | 44 | ); 45 | }; 46 | 47 | export default FloatingButton; 48 | -------------------------------------------------------------------------------- /src/components/GeneralCarousel/Carousel.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const containerStyling = (width: number, height: number) => { 6 | return css({ 7 | position: 'relative', 8 | 9 | width, 10 | height, 11 | minWidth: width, 12 | minHeight: height, 13 | 14 | borderRadius: Theme.borderRadius.medium, 15 | 16 | overflow: 'hidden', 17 | 18 | '& *': { 19 | userSelect: 'none', 20 | }, 21 | }); 22 | }; 23 | 24 | export const sliderWrapperStyling = (width: number, height: number) => 25 | css({ 26 | display: 'flex', 27 | width: '100%', 28 | margin: 0, 29 | padding: 0, 30 | height, 31 | 32 | overflow: 'hidden', 33 | }); 34 | 35 | export const carouselItemStyling = (width: number, height: number) => 36 | css({ 37 | display: 'flex', 38 | 39 | '& > *': { 40 | objectFit: 'cover', 41 | width, 42 | height, 43 | }, 44 | }); 45 | 46 | export const itemWrapperStyling = (width: number, height: number) => { 47 | return css({ 48 | minWidth: width, 49 | width, 50 | minHeight: height, 51 | height, 52 | 53 | '& > img': { 54 | width, 55 | height, 56 | 57 | backgroundColor: Theme.color.gray200, 58 | 59 | objectFit: 'cover', 60 | }, 61 | }); 62 | }; 63 | 64 | export const buttonContainerStyling = (showOnHover: boolean) => 65 | css({ 66 | transition: 'opacity .1s ease-in', 67 | 68 | opacity: showOnHover ? 0 : 1, 69 | 70 | 'div:hover &': { 71 | opacity: 1, 72 | }, 73 | 74 | '& > button': { 75 | position: 'absolute', 76 | top: '50%', 77 | display: 'flex', 78 | justifyContent: 'center', 79 | alignItems: 'center', 80 | zIndex: Theme.zIndex.overlayTop, 81 | 82 | width: '28px', 83 | height: '28px', 84 | border: 'none', 85 | borderRadius: '50%', 86 | outline: '0', 87 | 88 | backgroundColor: Theme.color.white, 89 | boxShadow: Theme.boxShadow.shadow8, 90 | 91 | transform: 'translateY(-50%)', 92 | 93 | cursor: 'pointer', 94 | 95 | '& svg': { 96 | width: '12px', 97 | height: '12px', 98 | 99 | '& path': { 100 | strokeWidth: 2, 101 | }, 102 | }, 103 | }, 104 | }); 105 | 106 | export const leftButtonStyling = css({ 107 | left: Theme.spacer.spacing2, 108 | }); 109 | 110 | export const rightButtonStyling = css({ 111 | right: Theme.spacer.spacing2, 112 | }); 113 | 114 | export const dotStyling = (isSelected: boolean) => { 115 | return css({ 116 | width: '6px', 117 | height: '6px', 118 | borderRadius: '50%', 119 | 120 | backgroundColor: Theme.color.white, 121 | 122 | opacity: isSelected ? 1 : 0.6, 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/GeneralCarousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import LeftIcon from '@assets/svg/left-icon.svg'; 2 | import RightIcon from '@assets/svg/right-icon.svg'; 3 | import { createContext, useMemo } from 'react'; 4 | import type { PropsWithChildren } from 'react'; 5 | 6 | import useCarousel from '@hooks/useCarousel'; 7 | 8 | import Box from '@components/Box/Box'; 9 | 10 | import { 11 | buttonContainerStyling, 12 | containerStyling, 13 | leftButtonStyling, 14 | rightButtonStyling, 15 | sliderWrapperStyling, 16 | } from './Carousel.style'; 17 | import CarouselItem from './CarouselItem'; 18 | import Dots from './Dots'; 19 | 20 | export interface CarouselProps extends PropsWithChildren { 21 | width: number; 22 | height: number; 23 | length: number; 24 | showNavigationOnHover?: boolean; 25 | showArrows?: boolean; 26 | showDots?: boolean; 27 | children?: JSX.Element | JSX.Element[]; 28 | } 29 | 30 | export const CarouselContext = createContext<{ 31 | viewIndex: number; 32 | width: number; 33 | height: number; 34 | itemRef: React.MutableRefObject; 35 | } | null>(null); 36 | 37 | const Carousel = ({ 38 | width, 39 | height, 40 | length, 41 | showNavigationOnHover = true, 42 | showArrows = true, 43 | showDots = true, 44 | children, 45 | }: CarouselProps) => { 46 | const { viewIndex, itemRef, carouselBoxRef, handleMoveImage, handleClickLeft, handleClickRight } = 47 | useCarousel(length); 48 | 49 | const context = useMemo( 50 | () => ({ 51 | width, 52 | height, 53 | viewIndex, 54 | itemRef, 55 | carouselBoxRef, 56 | handleMoveImage, 57 | handleClickLeft, 58 | handleClickRight, 59 | }), 60 | [ 61 | width, 62 | height, 63 | viewIndex, 64 | itemRef, 65 | carouselBoxRef, 66 | handleMoveImage, 67 | handleClickLeft, 68 | handleClickRight, 69 | ] 70 | ); 71 | 72 | return ( 73 | 74 |
75 | {showArrows && length !== 1 && ( 76 |
77 | 80 | 83 |
84 | )} 85 | 86 | {showDots && ( 87 | 88 | )} 89 | 90 | {children} 91 |
92 |
93 | ); 94 | }; 95 | 96 | Carousel.Item = CarouselItem; 97 | 98 | export default Carousel; 99 | -------------------------------------------------------------------------------- /src/components/GeneralCarousel/CarouselItem.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef } from 'react'; 2 | import type { PropsWithChildren } from 'react'; 3 | 4 | import { CarouselContext } from '@components/GeneralCarousel/Carousel'; 5 | import { carouselItemStyling } from '@components/GeneralCarousel/Carousel.style'; 6 | 7 | export interface CarouselItemProps extends PropsWithChildren { 8 | index: number; 9 | } 10 | 11 | const CarouselItem = ({ index, children }: CarouselItemProps) => { 12 | const ref = useRef(null); 13 | const context = useContext(CarouselContext); 14 | 15 | if (!context) throw Error('Carousel.Item is only available within Carousel.'); 16 | 17 | const { width, height, viewIndex, itemRef } = context; 18 | 19 | useEffect(() => { 20 | if (ref.current) { 21 | if (index === viewIndex) itemRef.current = ref.current; 22 | } 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, [viewIndex]); 25 | 26 | return ( 27 |
28 | {children} 29 |
30 | ); 31 | }; 32 | 33 | export default CarouselItem; 34 | -------------------------------------------------------------------------------- /src/components/GeneralCarousel/Dots.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import type { MouseEvent } from 'react'; 3 | 4 | import { Theme } from '@styles/Theme'; 5 | 6 | interface DotsProps { 7 | imageLength: number; 8 | activeNumber: number; 9 | moveImage: (imageNumber: number) => void; 10 | } 11 | 12 | const Dots = ({ imageLength, activeNumber, moveImage }: DotsProps) => { 13 | const images = Array.from({ length: imageLength }, () => ''); 14 | 15 | return ( 16 |
17 | {images.map((_, index) => { 18 | if (activeNumber === index) 19 | return ( 20 |
43 | ); 44 | }; 45 | 46 | export default Dots; 47 | 48 | const dotContainerStyling = css({ 49 | position: 'absolute', 50 | display: 'flex', 51 | gap: Theme.spacer.spacing2, 52 | 53 | left: '50%', 54 | bottom: Theme.spacer.spacing3, 55 | 56 | transform: 'translateX(-50%)', 57 | transition: 'opacity .1s ease-in', 58 | 59 | cursor: 'pointer', 60 | 61 | '.image-carousel-container:hover &': { 62 | opacity: 1, 63 | }, 64 | }); 65 | 66 | const dotStyle = (isSelected: boolean) => { 67 | return css({ 68 | width: '6px', 69 | height: '6px', 70 | 71 | backgroundColor: Theme.color.white, 72 | borderRadius: '50%', 73 | border: 'none', 74 | 75 | opacity: isSelected ? 1 : 0.6, 76 | cursor: 'pointer', 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/Heading/Heading.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { HeadingProps } from '@components/Heading/Heading'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getSizeStyling = (size: Required['size']) => { 8 | const style = { 9 | xxLarge: css({ 10 | fontSize: Theme.heading.xxLarge.fontSize, 11 | lineHeight: Theme.heading.xxLarge.lineHeight, 12 | }), 13 | xLarge: css({ 14 | fontSize: Theme.heading.xLarge.fontSize, 15 | lineHeight: Theme.heading.xLarge.lineHeight, 16 | }), 17 | large: css({ 18 | fontSize: Theme.heading.large.fontSize, 19 | lineHeight: Theme.heading.large.lineHeight, 20 | }), 21 | medium: css({ 22 | fontSize: Theme.heading.medium.fontSize, 23 | lineHeight: Theme.heading.medium.lineHeight, 24 | }), 25 | small: css({ 26 | fontSize: Theme.heading.small.fontSize, 27 | lineHeight: Theme.heading.small.lineHeight, 28 | }), 29 | xSmall: css({ 30 | fontSize: Theme.heading.xSmall.fontSize, 31 | lineHeight: Theme.heading.xSmall.lineHeight, 32 | }), 33 | }; 34 | 35 | return style[size]; 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/Heading/Heading.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithoutRef } from 'react'; 3 | 4 | import { getSizeStyling } from '@components/Heading/Heading.style'; 5 | 6 | export interface HeadingProps extends ComponentPropsWithoutRef<'h4'> { 7 | size?: Size; 8 | } 9 | 10 | const TAG_BY_SIZE = { 11 | xxLarge: 'h1', 12 | xLarge: 'h2', 13 | large: 'h3', 14 | medium: 'h4', 15 | small: 'h5', 16 | xSmall: 'h6', 17 | } as const; 18 | 19 | const Heading = ({ size = 'medium', children, ...attributes }: HeadingProps) => { 20 | const HeadingTag = TAG_BY_SIZE[size]; 21 | 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default Heading; 30 | -------------------------------------------------------------------------------- /src/components/ImageCarousel/ImageCarousel.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const getContainerStyling = (width: number, height: number) => { 6 | return css({ 7 | position: 'relative', 8 | 9 | minWidth: width, 10 | width, 11 | minHeight: height, 12 | height, 13 | borderRadius: Theme.borderRadius.medium, 14 | 15 | overflow: 'hidden', 16 | 17 | cursor: 'grab', 18 | 19 | '& *': { 20 | userSelect: 'none', 21 | }, 22 | }); 23 | }; 24 | 25 | export const sliderWrapperStyling = { 26 | width: '100%', 27 | margin: 0, 28 | padding: 0, 29 | 30 | overflow: 'hidden', 31 | }; 32 | 33 | export const getSliderContainerStyling = ( 34 | currentPosition: number, 35 | width: number, 36 | translateX: number, 37 | animate: boolean 38 | ) => { 39 | return css({ 40 | display: 'flex', 41 | 42 | width: '100%', 43 | height: '100%', 44 | 45 | transform: `translateX(${-currentPosition * width + translateX}px)`, 46 | transition: `transform ${animate ? 300 : 0}ms ease-in-out 0s`, 47 | }); 48 | }; 49 | 50 | export const getImageWrapperStyling = (width: number, height: number) => { 51 | return css({ 52 | minWidth: width, 53 | width, 54 | minHeight: height, 55 | height, 56 | 57 | '& > img': { 58 | width, 59 | height, 60 | 61 | backgroundColor: Theme.color.gray200, 62 | 63 | objectFit: 'cover', 64 | }, 65 | }); 66 | }; 67 | 68 | export const getButtonContainerStyling = (showOnHover: boolean) => 69 | css({ 70 | transition: 'opacity .1s ease-in', 71 | 72 | opacity: showOnHover ? 0 : 1, 73 | 74 | '.image-carousel-container:hover &': { 75 | opacity: 0.8, 76 | 77 | '&:hover': { 78 | opacity: 1, 79 | }, 80 | }, 81 | 82 | '& > button': { 83 | position: 'absolute', 84 | top: '50%', 85 | display: 'flex', 86 | justifyContent: 'center', 87 | alignItems: 'center', 88 | zIndex: Theme.zIndex.overlayTop, 89 | 90 | width: '28px', 91 | height: '28px', 92 | border: 'none', 93 | borderRadius: '50%', 94 | outline: '0', 95 | 96 | backgroundColor: Theme.color.white, 97 | boxShadow: Theme.boxShadow.shadow8, 98 | 99 | transform: 'translateY(-50%)', 100 | 101 | '&:hover': { 102 | cursor: 'pointer', 103 | }, 104 | 105 | '& svg': { 106 | width: '12px', 107 | height: '12px', 108 | 109 | '& path': { 110 | strokeWidth: 2, 111 | }, 112 | }, 113 | }, 114 | }); 115 | 116 | export const leftButtonStyling = css({ 117 | left: Theme.spacer.spacing2, 118 | }); 119 | 120 | export const rightButtonStyling = css({ 121 | right: Theme.spacer.spacing2, 122 | }); 123 | 124 | export const getDotsWrapperStyling = (showOnHover: boolean) => { 125 | return css({ 126 | position: 'absolute', 127 | left: '50%', 128 | bottom: Theme.spacer.spacing3, 129 | display: 'flex', 130 | gap: Theme.spacer.spacing2, 131 | 132 | transform: 'translateX(-50%)', 133 | transition: 'opacity .1s ease-in', 134 | 135 | opacity: showOnHover ? 0 : 1, 136 | cursor: 'pointer', 137 | 138 | '.image-carousel-container:hover &': { 139 | opacity: 1, 140 | }, 141 | }); 142 | }; 143 | 144 | export const dotStyling = (isSelected: boolean) => { 145 | return css({ 146 | width: '6px', 147 | height: '6px', 148 | borderRadius: '50%', 149 | 150 | backgroundColor: Theme.color.white, 151 | 152 | opacity: isSelected ? 1 : 0.6, 153 | }); 154 | }; 155 | -------------------------------------------------------------------------------- /src/components/ImageCarousel/ImageCarousel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | import LeftIcon from '@assets/svg/left-icon.svg'; 3 | import RightIcon from '@assets/svg/right-icon.svg'; 4 | 5 | import { useImageCarousel } from '@hooks/useImageCarousel'; 6 | 7 | import Box from '@components/Box/Box'; 8 | import { 9 | dotStyling, 10 | getButtonContainerStyling, 11 | getContainerStyling, 12 | getDotsWrapperStyling, 13 | getImageWrapperStyling, 14 | getSliderContainerStyling, 15 | leftButtonStyling, 16 | rightButtonStyling, 17 | sliderWrapperStyling, 18 | } from '@components/ImageCarousel/ImageCarousel.style'; 19 | 20 | export interface ImageCarouselProps { 21 | width: number; 22 | height: number; 23 | images: string[]; 24 | isDraggable?: boolean; 25 | showArrows?: boolean; 26 | showDots?: boolean; 27 | showNavigationOnHover?: boolean; 28 | } 29 | 30 | const ImageCarousel = ({ 31 | width, 32 | height, 33 | images, 34 | isDraggable = false, 35 | showArrows = false, 36 | showDots = false, 37 | showNavigationOnHover = false, 38 | }: ImageCarouselProps) => { 39 | const { 40 | sliderRef, 41 | animate, 42 | currentPosition, 43 | translateX, 44 | handleSliderNavigationClick, 45 | handleSliderNavigationEnterKeyPress, 46 | handlerSliderMoueDown, 47 | handleSliderTouchStart, 48 | handleSliderTransitionEnd, 49 | } = useImageCarousel(width, images.length); 50 | 51 | return ( 52 | 58 | 59 |
66 | {images.map((imageUrl, index) => ( 67 | // eslint-disable-next-line react/no-array-index-key 68 |
69 | 이미지 70 |
71 | ))} 72 |
73 |
74 | {showArrows && images.length !== 1 && ( 75 |
76 | 84 | 92 |
93 | )} 94 | {showDots && ( 95 |
96 | {Array.from({ length: images.length }, (_, index) => ( 97 | 106 | ))} 107 |
108 | )} 109 |
110 | ); 111 | }; 112 | 113 | export default ImageCarousel; 114 | -------------------------------------------------------------------------------- /src/components/ImageUploadInput/ImageUploadInput.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const inputContainerStyling = css({ 6 | display: 'flex', 7 | flexDirection: 'column', 8 | gap: Theme.spacer.spacing2, 9 | }); 10 | 11 | export const inputWrapperStyling = css({ 12 | overflowX: 'auto', 13 | overflowY: 'hidden', 14 | }); 15 | 16 | export const getUploadButtonStyling = (isUploaded: boolean, maxUploaded: boolean) => { 17 | return css({ 18 | display: maxUploaded ? 'none' : 'flex', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | gap: Theme.spacer.spacing2, 22 | 23 | width: isUploaded ? '100px' : '100%', 24 | minWidth: isUploaded ? '100px' : '100%', 25 | height: '100px', 26 | 27 | fontWeight: 'normal', 28 | 29 | transition: 'none', 30 | }); 31 | }; 32 | 33 | export const inputStyling = css({ 34 | display: 'none', 35 | }); 36 | 37 | export const imageWrapperStyling = css({ 38 | position: 'relative', 39 | 40 | minWidth: '100px', 41 | width: '100px', 42 | minHeight: '100px', 43 | height: '100px', 44 | }); 45 | 46 | export const imageStyling = css({ 47 | width: '100px', 48 | height: '100px', 49 | borderRadius: Theme.borderRadius.small, 50 | 51 | objectFit: 'cover', 52 | }); 53 | 54 | export const deleteButtonStyling = css({ 55 | position: 'absolute', 56 | right: 0, 57 | 58 | width: '36px', 59 | height: '36px', 60 | border: 'none', 61 | borderRadius: 0, 62 | borderTopRightRadius: Theme.borderRadius.small, 63 | outline: 0, 64 | 65 | backgroundColor: Theme.color.black, 66 | 67 | transition: 'all .2s ease-in', 68 | 69 | cursor: 'pointer', 70 | 71 | '&:hover': { 72 | backgroundColor: Theme.color.gray800, 73 | }, 74 | 75 | '& svg': { 76 | width: '12px', 77 | height: '12px', 78 | 79 | '& > path': { 80 | stroke: Theme.color.white, 81 | }, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/ImageUploadInput/ImageUploadInput.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from '@assets/svg/close-icon.svg'; 2 | import ImageIcon from '@assets/svg/image-icon.svg'; 3 | import type { ComponentPropsWithoutRef } from 'react'; 4 | import { useRef } from 'react'; 5 | 6 | import Box from '@components/Box/Box'; 7 | import Button from '@components/Button/Button'; 8 | import Flex from '@components/Flex/Flex'; 9 | import { 10 | deleteButtonStyling, 11 | getUploadButtonStyling, 12 | imageStyling, 13 | imageWrapperStyling, 14 | inputContainerStyling, 15 | inputStyling, 16 | inputWrapperStyling, 17 | } from '@components/ImageUploadInput/ImageUploadInput.style'; 18 | import Label from '@components/Label/Label'; 19 | import SupportingText from '@components/SupportingText/SupportingText'; 20 | 21 | import { Theme } from '@styles/Theme'; 22 | 23 | export interface ImageUploadInputProps extends ComponentPropsWithoutRef<'input'> { 24 | label?: string; 25 | supportingText?: string; 26 | imageUrls: string[] | null; 27 | imageAltText: string; 28 | maxUploadCount?: number; 29 | onRemove?: CallableFunction; 30 | } 31 | 32 | const ImageUploadInput = ({ 33 | id, 34 | label, 35 | supportingText, 36 | imageUrls, 37 | imageAltText, 38 | multiple, 39 | maxUploadCount, 40 | onRemove, 41 | ...attributes 42 | }: ImageUploadInputProps) => { 43 | const inputRef = useRef(null); 44 | 45 | const handleImageUploadButton = () => { 46 | inputRef.current?.click(); 47 | }; 48 | 49 | return ( 50 |
51 | {label && } 52 | 53 | 64 | 73 | {imageUrls && 74 | imageUrls.map((imageUrl) => ( 75 | 76 | {imageAltText} 77 | 85 | 86 | ))} 87 | 88 | {supportingText && {supportingText}} 89 |
90 | ); 91 | }; 92 | 93 | export default ImageUploadInput; 94 | -------------------------------------------------------------------------------- /src/components/Input/Input.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { InputProps } from '@components/Input/Input'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const inputContainerStyling = css({ 8 | display: 'flex', 9 | flexDirection: 'column', 10 | gap: Theme.spacer.spacing2, 11 | }); 12 | 13 | export const inputWrapperStyling = (isError: Required['isError']) => { 14 | return css({ 15 | display: 'flex', 16 | gap: '12px', 17 | alignItems: 'center', 18 | 19 | paddingTop: 0, 20 | paddingBottom: 0, 21 | borderRadius: Theme.borderRadius.small, 22 | 23 | backgroundColor: isError ? `${Theme.color.red100} !important` : 'transparent', 24 | 25 | transition: 'all .2s ease-in', 26 | 27 | '&:focus-within': { 28 | backgroundColor: isError ? Theme.color.red100 : Theme.color.white, 29 | boxShadow: isError 30 | ? `inset 0 0 0 1px ${Theme.color.red200}` 31 | : `inset 0 0 0 1px ${Theme.color.gray300}`, 32 | }, 33 | 34 | '& svg': { 35 | width: '16px', 36 | height: '16px', 37 | }, 38 | }); 39 | }; 40 | 41 | export const getVariantStyling = (variant: Required['variant']) => { 42 | const style = { 43 | default: css({ 44 | backgroundColor: Theme.color.gray100, 45 | }), 46 | 47 | text: css({ 48 | backgroundColor: 'transparent', 49 | }), 50 | }; 51 | 52 | return style[variant]; 53 | }; 54 | 55 | export const getSizeStyling = (size: Required['size']) => { 56 | const style = { 57 | large: css({ 58 | padding: '14px 16px', 59 | 60 | fontSize: Theme.text.medium.fontSize, 61 | lineHeight: Theme.text.medium.lineHeight, 62 | }), 63 | 64 | medium: css({ 65 | padding: '12px 16px', 66 | 67 | fontSize: Theme.text.medium.fontSize, 68 | lineHeight: Theme.text.medium.lineHeight, 69 | }), 70 | 71 | small: css({ 72 | padding: '8px 12px', 73 | 74 | fontSize: Theme.text.small.fontSize, 75 | lineHeight: Theme.text.small.lineHeight, 76 | }), 77 | }; 78 | 79 | return style[size]; 80 | }; 81 | 82 | export const getInputStyling = css({ 83 | width: '100%', 84 | paddingLeft: 0, 85 | paddingRight: 0, 86 | border: 'none', 87 | borderRadius: Theme.borderRadius.small, 88 | outline: 0, 89 | 90 | backgroundColor: 'transparent', 91 | }); 92 | -------------------------------------------------------------------------------- /src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithRef, ForwardedRef, ReactElement } from 'react'; 3 | import { forwardRef } from 'react'; 4 | 5 | import { 6 | getInputStyling, 7 | getSizeStyling, 8 | getVariantStyling, 9 | inputContainerStyling, 10 | inputWrapperStyling, 11 | } from '@components/Input/Input.style'; 12 | import Label from '@components/Label/Label'; 13 | import SupportingText from '@components/SupportingText/SupportingText'; 14 | 15 | export interface InputProps extends Omit, 'size'> { 16 | label?: string; 17 | variant?: 'default' | 'text'; 18 | size?: Extract; 19 | isError?: boolean; 20 | icon?: ReactElement; 21 | supportingText?: string; 22 | } 23 | 24 | const Input = ( 25 | { 26 | label, 27 | variant = 'default', 28 | size = 'medium', 29 | isError = false, 30 | icon, 31 | supportingText, 32 | ...attributes 33 | }: InputProps, 34 | ref: ForwardedRef 35 | ) => { 36 | return ( 37 |
38 | {label && ( 39 | 42 | )} 43 |
44 | {icon} 45 | 46 |
47 | {supportingText && {supportingText}} 48 |
49 | ); 50 | }; 51 | 52 | export default forwardRef(Input); 53 | -------------------------------------------------------------------------------- /src/components/Label/Label.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const labelStyling = css({ 6 | fontSize: Theme.text.small.fontSize, 7 | lineHeight: Theme.text.small.lineHeight, 8 | fontWeight: 600, 9 | }); 10 | 11 | export const requiredStyling = css({ 12 | color: Theme.color.red300, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { labelStyling, requiredStyling } from '@components/Label/Label.style'; 4 | 5 | export interface LabelProps extends ComponentPropsWithoutRef<'label'> { 6 | required?: boolean; 7 | } 8 | 9 | const Label = ({ id, required = false, children, ...attributes }: LabelProps) => { 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | export default Label; 23 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const menuStyling = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | alignItems: 'flex-end', 7 | }); 8 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import type { ComponentPropsWithoutRef } from 'react'; 3 | 4 | import { menuStyling } from '@components/Menu/Menu.style'; 5 | 6 | export interface MenuProps extends ComponentPropsWithoutRef<'div'> { 7 | closeMenu: () => void; 8 | } 9 | 10 | const Menu = ({ children, closeMenu, ...attributes }: MenuProps) => { 11 | const menuRef = useRef(null); 12 | 13 | const handleBackdropClick = useCallback( 14 | (event: globalThis.MouseEvent) => { 15 | if (!menuRef.current?.contains(event.target as Node)) { 16 | closeMenu(); 17 | } 18 | }, 19 | [closeMenu] 20 | ); 21 | 22 | const handleEscClick = useCallback( 23 | (event: globalThis.KeyboardEvent) => { 24 | if (event.key === 'Escape') { 25 | closeMenu(); 26 | } 27 | }, 28 | [closeMenu] 29 | ); 30 | 31 | useEffect(() => { 32 | window.addEventListener('click', handleBackdropClick); 33 | window.addEventListener('keydown', handleEscClick); 34 | 35 | return () => { 36 | window.removeEventListener('click', handleBackdropClick); 37 | window.removeEventListener('keydown', handleEscClick); 38 | }; 39 | }, [handleBackdropClick, handleEscClick]); 40 | 41 | return ( 42 |
43 | {children} 44 |
45 | ); 46 | }; 47 | 48 | export default Menu; 49 | -------------------------------------------------------------------------------- /src/components/MenuItem/MenuItem.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const menuItemStyling = css({ 6 | padding: Theme.spacer.spacing2, 7 | 8 | fontSize: Theme.text.small.fontSize, 9 | lineHeight: Theme.text.small.lineHeight, 10 | 11 | transition: 'all .2s ease-in', 12 | 13 | cursor: 'pointer', 14 | 15 | '&:hover': { 16 | backgroundColor: Theme.color.gray100, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/MenuItem/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithRef, ForwardedRef, KeyboardEvent } from 'react'; 2 | import { forwardRef, useCallback } from 'react'; 3 | 4 | import { menuItemStyling } from '@components/MenuItem/MenuItem.style'; 5 | 6 | interface MenuItemProps extends ComponentPropsWithRef<'li'> { 7 | /** 메뉴 아이템을 클릭했을 때 실행시킬 함수 */ 8 | onClick: () => void; 9 | } 10 | 11 | const MenuItem = ( 12 | { children, onClick, ...attributes }: MenuItemProps, 13 | ref: ForwardedRef 14 | ) => { 15 | const handleEnterKeyPress = useCallback( 16 | (event: KeyboardEvent) => { 17 | if (event.key === 'Enter') { 18 | onClick(); 19 | } 20 | }, 21 | [onClick] 22 | ); 23 | 24 | return ( 25 |
  • 35 | {children} 36 |
  • 37 | ); 38 | }; 39 | 40 | export default forwardRef(MenuItem); 41 | -------------------------------------------------------------------------------- /src/components/MenuList/MenuList.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const menuListStyling = css({ 6 | position: 'absolute', 7 | transform: 'translateY(49px)', 8 | display: 'flex', 9 | flexDirection: 'column', 10 | zIndex: Theme.zIndex.overlayMiddle, 11 | 12 | minWidth: '150px', 13 | 14 | padding: `${Theme.spacer.spacing2} 0`, 15 | marginTop: Theme.spacer.spacing2, 16 | 17 | backgroundColor: Theme.color.white, 18 | borderRadius: Theme.borderRadius.small, 19 | 20 | boxShadow: Theme.boxShadow.shadow8, 21 | 22 | transition: 'all .2 ease-in', 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/MenuList/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 2 | import { forwardRef } from 'react'; 3 | 4 | import { menuListStyling } from '@components/MenuList/MenuList.style'; 5 | 6 | type MenuListProps = ComponentPropsWithRef<'ul'>; 7 | 8 | const MenuList = ( 9 | { children, ...attributes }: MenuListProps, 10 | ref: ForwardedRef 11 | ) => { 12 | return ( 13 |
      14 | {children} 15 |
    16 | ); 17 | }; 18 | 19 | export default forwardRef(MenuList); 20 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | import { fadeIn } from '@styles/animation'; 5 | 6 | export const backdropStyling = css({ 7 | position: 'fixed', 8 | top: 0, 9 | left: 0, 10 | zIndex: Theme.zIndex.overlayMiddle, 11 | 12 | width: '100%', 13 | height: '100%', 14 | 15 | backgroundColor: 'rgba(0, 0, 0, .15)', 16 | 17 | cursor: 'pointer', 18 | }); 19 | 20 | export const dialogStyling = css({ 21 | position: 'fixed', 22 | top: '50%', 23 | transform: 'translateY(-50%)', 24 | zIndex: Theme.zIndex.overlayTop, 25 | display: 'flex', 26 | flexDirection: 'column', 27 | justifyContent: 'center', 28 | alignItems: 'center', 29 | 30 | minWidth: '300px', 31 | padding: Theme.spacer.spacing4, 32 | margin: '0 auto', 33 | 34 | border: 'none', 35 | borderRadius: Theme.borderRadius.large, 36 | 37 | backgroundColor: Theme.color.white, 38 | boxShadow: Theme.boxShadow.shadow8, 39 | 40 | animation: `${fadeIn} 0.2s ease-in`, 41 | }); 42 | 43 | export const closeButtonStyling = css({ 44 | position: 'absolute', 45 | right: '24px', 46 | top: '24px', 47 | alignSelf: 'flex-end', 48 | 49 | marginBottom: Theme.spacer.spacing1, 50 | 51 | border: 'none', 52 | backgroundColor: 'transparent', 53 | 54 | cursor: 'pointer', 55 | }); 56 | 57 | export const closeIconStyling = css({ 58 | width: '16px', 59 | height: '16px', 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | 3 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 4 | import CloseIcon from '@assets/svg/close-icon.svg'; 5 | import type { ComponentPropsWithoutRef } from 'react'; 6 | import { useCallback, useEffect, useRef } from 'react'; 7 | import { createPortal } from 'react-dom'; 8 | 9 | import { 10 | backdropStyling, 11 | closeButtonStyling, 12 | closeIconStyling, 13 | dialogStyling, 14 | } from '@components/Modal/Modal.style'; 15 | 16 | export interface ModalProps extends ComponentPropsWithoutRef<'dialog'> { 17 | /** 18 | * Modal이 열려있는지에 대한 상태 19 | * 20 | * @default false 21 | */ 22 | isOpen: boolean; 23 | /** 24 | * Modal에 닫기버튼에 대한 여부 25 | * 26 | * @default true 27 | */ 28 | hasCloseButton?: boolean; 29 | /** 30 | * Modal Backdrop을 클릭해서 Modal을 닫을 수 있는 지에 대한 여부 31 | * @default true 32 | */ 33 | isBackdropClosable?: boolean; 34 | /** Modal을 닫는 함수 */ 35 | closeModal: () => void; 36 | } 37 | 38 | const Modal = ({ 39 | closeModal, 40 | isOpen = false, 41 | hasCloseButton = true, 42 | isBackdropClosable = true, 43 | children, 44 | ...attributes 45 | }: ModalProps) => { 46 | const handleEscKeyPress = useCallback( 47 | (event: KeyboardEvent) => { 48 | if (event.key === 'Escape' && isBackdropClosable) { 49 | closeModal(); 50 | } 51 | }, 52 | [closeModal] 53 | ); 54 | 55 | useEffect(() => { 56 | if (isOpen) { 57 | document.body.style.overflow = 'hidden'; 58 | window.addEventListener('keydown', handleEscKeyPress); 59 | } 60 | 61 | return () => { 62 | document.body.style.overflow = 'auto'; 63 | window.removeEventListener('keydown', handleEscKeyPress); 64 | }; 65 | }, [isOpen, handleEscKeyPress]); 66 | 67 | return createPortal( 68 | // eslint-disable-next-line react/jsx-no-useless-fragment 69 | <> 70 | {isOpen && ( 71 | <> 72 |
    {}} /> 73 | 74 | {hasCloseButton && ( 75 | 83 | )} 84 | {children} 85 | 86 | 87 | )} 88 | , 89 | document.body 90 | ); 91 | }; 92 | 93 | export default Modal; 94 | -------------------------------------------------------------------------------- /src/components/RadioButton/RadioButton.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const radioContainerStyling = css({ 6 | display: 'flex', 7 | flexDirection: 'column', 8 | gap: Theme.spacer.spacing2, 9 | }); 10 | 11 | export const radioWrapperStyling = css({ 12 | display: 'flex', 13 | }); 14 | 15 | export const labelStyling = css({ 16 | display: 'inline-flex', 17 | alignItems: 'center', 18 | marginRight: Theme.spacer.spacing2, 19 | 20 | cursor: 'pointer', 21 | }); 22 | 23 | export const inputStyling = css({ 24 | display: 'none', 25 | position: 'absolute', 26 | 27 | outline: '1px solid black', 28 | 29 | opacity: 0, 30 | pointerEvents: 'none', 31 | }); 32 | 33 | export const buttonStyling = css({ 34 | width: '16px', 35 | height: '16px', 36 | 37 | border: `2px solid ${Theme.color.gray200}`, 38 | borderRadius: '50%', 39 | boxSizing: 'border-box', 40 | 41 | marginRight: Theme.spacer.spacing2, 42 | 43 | 'input[type="radio"]:checked + &': { 44 | border: `5px solid ${Theme.color.blue600}`, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/RadioButton/RadioButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import type { ChangeEvent, ComponentPropsWithoutRef, KeyboardEvent } from 'react'; 3 | 4 | import Label from '@components/Label/Label'; 5 | import { 6 | buttonStyling, 7 | inputStyling, 8 | labelStyling, 9 | radioContainerStyling, 10 | radioWrapperStyling, 11 | } from '@components/RadioButton/RadioButton.style'; 12 | import SupportingText from '@components/SupportingText/SupportingText'; 13 | 14 | export interface RadioButtonProps extends ComponentPropsWithoutRef<'input'> { 15 | /** Radio에서 처음 선택된 초기 값 */ 16 | initialCheckedOption?: string; 17 | /** Radio에서 선택할 수 있는 문자열 option */ 18 | options: string[]; 19 | /** RadioButton의 라벨 텍스트 */ 20 | label?: string; 21 | /** RadioButton의 부가 정보 텍스트 */ 22 | supportingText?: string; 23 | /** 라디오 버튼들을 하나로 묶어주는 이름 */ 24 | name?: string; 25 | } 26 | 27 | const RadioButton = ({ 28 | initialCheckedOption, 29 | options, 30 | label, 31 | supportingText, 32 | name = 'sample', 33 | onChange, 34 | onKeyDown, 35 | ...attributes 36 | }: RadioButtonProps) => { 37 | const [checkedOption, setCheckedOption] = useState(initialCheckedOption ?? options[0]); 38 | 39 | const handleOptionClick = (e: ChangeEvent) => { 40 | onChange?.(e); 41 | 42 | setCheckedOption(e.target.id); 43 | }; 44 | 45 | const handleOptionEnter = (e: KeyboardEvent) => { 46 | if (e.key === 'Enter') { 47 | onKeyDown?.(e); 48 | 49 | setCheckedOption(e.currentTarget.id); 50 | } 51 | }; 52 | 53 | return ( 54 |
    55 | {label && ( 56 | 59 | )} 60 |
    61 | {options.map((option, index) => ( 62 |
    86 | ); 87 | }; 88 | 89 | export default RadioButton; 90 | -------------------------------------------------------------------------------- /src/components/SVGCarousel/SVGCarousel.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const getContainerStyling = (width: number, height: number) => { 6 | return css({ 7 | position: 'relative', 8 | 9 | minWidth: width, 10 | width, 11 | minHeight: height, 12 | height, 13 | borderRadius: Theme.borderRadius.medium, 14 | 15 | overflow: 'hidden', 16 | 17 | '& *': { 18 | userSelect: 'none', 19 | }, 20 | }); 21 | }; 22 | 23 | export const sliderWrapperStyling = { 24 | width: '100%', 25 | margin: 0, 26 | padding: 0, 27 | 28 | overflow: 'hidden', 29 | }; 30 | 31 | export const getSliderContainerStyling = ( 32 | currentPosition: number, 33 | width: number, 34 | translateX: number, 35 | animate: boolean 36 | ) => { 37 | return css({ 38 | display: 'flex', 39 | 40 | width: '100%', 41 | height: '100%', 42 | 43 | transform: `translateX(${-currentPosition * width + translateX}px)`, 44 | transition: `transform ${animate ? 300 : 0}ms ease-in-out 0s`, 45 | }); 46 | }; 47 | 48 | export const getSVGWrapperStyling = (width: number, height: number) => { 49 | return css({ 50 | minWidth: width, 51 | width, 52 | minHeight: height, 53 | height, 54 | 55 | '& > img': { 56 | width, 57 | height, 58 | 59 | backgroundColor: Theme.color.gray200, 60 | 61 | objectFit: 'cover', 62 | }, 63 | }); 64 | }; 65 | 66 | export const getButtonContainerStyling = (showOnHover: boolean) => 67 | css({ 68 | transition: 'opacity .1s ease-in', 69 | 70 | opacity: showOnHover ? 0 : 1, 71 | 72 | 'div:hover &': { 73 | opacity: 1, 74 | }, 75 | 76 | '& > button': { 77 | position: 'absolute', 78 | top: '50%', 79 | display: 'flex', 80 | justifyContent: 'center', 81 | alignItems: 'center', 82 | zIndex: Theme.zIndex.overlayTop, 83 | 84 | width: '28px', 85 | height: '28px', 86 | border: 'none', 87 | borderRadius: '50%', 88 | outline: '0', 89 | 90 | backgroundColor: Theme.color.white, 91 | boxShadow: Theme.boxShadow.shadow8, 92 | 93 | transform: 'translateY(-50%)', 94 | 95 | '& svg': { 96 | width: '12px', 97 | height: '12px', 98 | 99 | '& path': { 100 | strokeWidth: 2, 101 | }, 102 | }, 103 | }, 104 | }); 105 | 106 | export const leftButtonStyling = css({ 107 | left: Theme.spacer.spacing2, 108 | }); 109 | 110 | export const rightButtonStyling = css({ 111 | right: Theme.spacer.spacing2, 112 | }); 113 | 114 | export const getDotsWrapperStyling = (showOnHover: boolean) => { 115 | return css({ 116 | position: 'absolute', 117 | left: '50%', 118 | bottom: Theme.spacer.spacing3, 119 | display: 'flex', 120 | gap: Theme.spacer.spacing2, 121 | 122 | transform: 'translateX(-50%)', 123 | transition: 'opacity .1s ease-in', 124 | 125 | opacity: showOnHover ? 0 : 1, 126 | cursor: 'pointer', 127 | 128 | 'div:hover &': { 129 | opacity: 1, 130 | }, 131 | }); 132 | }; 133 | 134 | export const dotStyling = (isSelected: boolean) => { 135 | return css({ 136 | width: '6px', 137 | height: '6px', 138 | borderRadius: '50%', 139 | 140 | backgroundColor: Theme.color.white, 141 | 142 | opacity: isSelected ? 1 : 0.6, 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /src/components/SVGCarousel/SVGCarousel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | import LeftIcon from '@assets/svg/left-icon.svg'; 3 | import RightIcon from '@assets/svg/right-icon.svg'; 4 | 5 | import { useImageCarousel } from '@hooks/useImageCarousel'; 6 | 7 | import Box from '@components/Box/Box'; 8 | import { 9 | dotStyling, 10 | getButtonContainerStyling, 11 | getContainerStyling, 12 | getDotsWrapperStyling, 13 | getSVGWrapperStyling, 14 | getSliderContainerStyling, 15 | leftButtonStyling, 16 | rightButtonStyling, 17 | sliderWrapperStyling, 18 | } from '@components/SVGCarousel/SVGCarousel.style'; 19 | 20 | export interface SVGCarouselProps { 21 | width: number; 22 | height: number; 23 | images: React.FC>[]; 24 | showArrows?: boolean; 25 | showDots?: boolean; 26 | showNavigationOnHover?: boolean; 27 | } 28 | 29 | const SVGCarousel = ({ 30 | width, 31 | height, 32 | images, 33 | showArrows = false, 34 | showDots = false, 35 | showNavigationOnHover = false, 36 | }: SVGCarouselProps) => { 37 | const { 38 | sliderRef, 39 | animate, 40 | currentPosition, 41 | translateX, 42 | handleSliderNavigationClick, 43 | handleSliderNavigationEnterKeyPress, 44 | handlerSliderMoueDown, 45 | handleSliderTouchStart, 46 | handleSliderTransitionEnd, 47 | } = useImageCarousel(width, images.length); 48 | 49 | return ( 50 | 51 | 52 |
    59 | {images.map((SVG, index) => ( 60 | // eslint-disable-next-line react/no-array-index-key 61 |
    62 | 63 |
    64 | ))} 65 |
    66 |
    67 | {showArrows && ( 68 |
    69 | 76 | 83 |
    84 | )} 85 | {showDots && ( 86 |
    87 | {Array.from({ length: images.length }, (_, index) => ( 88 | 97 | ))} 98 |
    99 | )} 100 |
    101 | ); 102 | }; 103 | 104 | export default SVGCarousel; 105 | -------------------------------------------------------------------------------- /src/components/SVGCarouselModal/SVGCarouselModal.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const boxStyling = (width: number, height: number) => { 6 | return css({ 7 | display: 'flex', 8 | flexDirection: 'column', 9 | alignItems: 'center', 10 | 11 | width: `${width}px`, 12 | height: `${height}px`, 13 | marginTop: '30px', 14 | 15 | '@media screen and (max-width: 600px)': { 16 | width: `${width}px`, 17 | marginBottom: Theme.spacer.spacing6, 18 | }, 19 | }); 20 | }; 21 | 22 | export const buttonStyling = (buttonGap: number) => { 23 | return css({ 24 | width: '100%', 25 | marginTop: `${buttonGap}px`, 26 | marginBottom: '10px', 27 | }); 28 | }; 29 | 30 | export const getContainerStyling = (width: number, height: number) => { 31 | return css({ 32 | position: 'relative', 33 | 34 | minWidth: width, 35 | width, 36 | minHeight: height, 37 | height, 38 | borderRadius: Theme.borderRadius.medium, 39 | 40 | overflow: 'hidden', 41 | 42 | '& *': { 43 | userSelect: 'none', 44 | }, 45 | }); 46 | }; 47 | 48 | export const sliderWrapperStyling = { 49 | width: '100%', 50 | margin: 0, 51 | padding: 0, 52 | 53 | overflow: 'hidden', 54 | }; 55 | 56 | export const getSliderContainerStyling = ( 57 | currentPosition: number, 58 | width: number, 59 | translateX: number, 60 | animate: boolean 61 | ) => { 62 | return css({ 63 | display: 'flex', 64 | 65 | width: '100%', 66 | height: '100%', 67 | 68 | transform: `translateX(${-currentPosition * width + translateX}px)`, 69 | transition: `transform ${animate ? 300 : 0}ms ease-in-out 0s`, 70 | }); 71 | }; 72 | 73 | export const getSVGWrapperStyling = (width: number, height: number) => { 74 | return css({ 75 | minWidth: width, 76 | width, 77 | minHeight: height, 78 | height, 79 | 80 | '& > img': { 81 | width, 82 | height, 83 | 84 | backgroundColor: Theme.color.gray200, 85 | 86 | objectFit: 'cover', 87 | }, 88 | }); 89 | }; 90 | 91 | export const getButtonContainerStyling = (showOnHover: boolean) => 92 | css({ 93 | transition: 'opacity .1s ease-in', 94 | 95 | opacity: showOnHover ? 0 : 1, 96 | 97 | 'div:hover &': { 98 | opacity: 1, 99 | }, 100 | 101 | '& > button': { 102 | position: 'absolute', 103 | top: '50%', 104 | display: 'flex', 105 | justifyContent: 'center', 106 | alignItems: 'center', 107 | zIndex: Theme.zIndex.overlayTop, 108 | 109 | width: '28px', 110 | height: '28px', 111 | border: 'none', 112 | borderRadius: '50%', 113 | outline: '0', 114 | 115 | backgroundColor: Theme.color.white, 116 | boxShadow: Theme.boxShadow.shadow8, 117 | 118 | transform: 'translateY(-50%)', 119 | 120 | '& svg': { 121 | width: '12px', 122 | height: '12px', 123 | 124 | '& path': { 125 | strokeWidth: 2, 126 | }, 127 | }, 128 | }, 129 | }); 130 | 131 | export const leftButtonStyling = css({ 132 | left: Theme.spacer.spacing2, 133 | }); 134 | 135 | export const rightButtonStyling = css({ 136 | right: Theme.spacer.spacing2, 137 | }); 138 | 139 | export const getDotsWrapperStyling = (showOnHover: boolean) => { 140 | return css({ 141 | position: 'absolute', 142 | left: '50%', 143 | bottom: Theme.spacer.spacing3, 144 | display: 'flex', 145 | gap: Theme.spacer.spacing2, 146 | 147 | transform: 'translateX(-50%)', 148 | transition: 'opacity .1s ease-in', 149 | 150 | opacity: showOnHover ? 0 : 1, 151 | cursor: 'pointer', 152 | 153 | 'div:hover &': { 154 | opacity: 1, 155 | }, 156 | }); 157 | }; 158 | 159 | export const dotStyling = (isSelected: boolean) => { 160 | return css({ 161 | width: '6px', 162 | height: '6px', 163 | borderRadius: '50%', 164 | 165 | backgroundColor: Theme.color.black, 166 | 167 | opacity: isSelected ? 1 : 0.6, 168 | }); 169 | }; 170 | -------------------------------------------------------------------------------- /src/components/Select/Select.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { SelectProps } from '@components/Select/Select'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const selectContainerStyling = css({ 8 | display: 'flex', 9 | flexDirection: 'column', 10 | gap: Theme.spacer.spacing2, 11 | }); 12 | 13 | export const getSelectWrapperStyling = (isError: Required['isError']) => { 14 | return css({ 15 | position: 'relative', 16 | paddingRight: Theme.spacer.spacing3, 17 | backgroundColor: isError ? Theme.color.red100 : Theme.color.gray100, 18 | border: `1px solid ${isError ? Theme.color.red200 : 'transparent'}`, 19 | borderRadius: Theme.borderRadius.small, 20 | }); 21 | }; 22 | 23 | export const getSizeStyling = (size: Required['size']) => { 24 | const style = { 25 | large: css({ 26 | padding: '14px 16px', 27 | 28 | fontSize: Theme.text.medium.fontSize, 29 | lineHeight: Theme.text.medium.lineHeight, 30 | }), 31 | 32 | medium: css({ 33 | padding: '12px 16px', 34 | 35 | fontSize: Theme.text.medium.fontSize, 36 | lineHeight: Theme.text.medium.lineHeight, 37 | }), 38 | 39 | small: css({ 40 | padding: '8px 12px', 41 | 42 | fontSize: Theme.text.small.fontSize, 43 | lineHeight: Theme.text.small.lineHeight, 44 | }), 45 | }; 46 | 47 | return style[size]; 48 | }; 49 | 50 | export const getSelectStyling = (isError: Required['isError']) => { 51 | return css({ 52 | width: '100%', 53 | backgroundColor: isError ? Theme.color.red100 : Theme.color.gray100, 54 | border: 'none', 55 | borderRadius: Theme.borderRadius.small, 56 | outline: 0, 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 3 | import { forwardRef } from 'react'; 4 | 5 | import Label from '@components/Label/Label'; 6 | import { 7 | getSelectStyling, 8 | getSelectWrapperStyling, 9 | getSizeStyling, 10 | selectContainerStyling, 11 | } from '@components/Select/Select.style'; 12 | import SupportingText from '@components/SupportingText/SupportingText'; 13 | 14 | export interface SelectProps extends Omit, 'size'> { 15 | /** Select의 라벨 텍스트 */ 16 | label?: string; 17 | /** 18 | * Select의 시이즈 19 | * 20 | * @default 'medium' 21 | */ 22 | size?: Extract; 23 | /** 24 | * Select 인풋의 에러 여부 25 | * 26 | * @default false 27 | */ 28 | isError?: boolean; 29 | /** Select에서 선택할 수 있는 JSX option 요소들 */ 30 | children: JSX.Element | JSX.Element[]; 31 | /** Select의 부가 정보 텍스트 */ 32 | supportingText?: string; 33 | } 34 | 35 | const Select = ( 36 | { label, size = 'medium', isError = false, children, supportingText, ...attributes }: SelectProps, 37 | ref: ForwardedRef 38 | ) => { 39 | return ( 40 |
    41 | {label && ( 42 | 45 | )} 46 |
    47 | 50 |
    51 | {supportingText && {supportingText}} 52 |
    53 | ); 54 | }; 55 | 56 | export default forwardRef(Select); 57 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton.style.ts: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | const skeletonAnimation = keyframes` 6 | 0% { 7 | background-position: 0% 50%; 8 | } 9 | 50% { 10 | background-position: 100% 50%; 11 | } 12 | 100% { 13 | background-position: 0% 50%; 14 | } 15 | `; 16 | 17 | export const getSkeletonStyling = (width: string, height: string, variant: 'square' | 'circle') => { 18 | return css({ 19 | width, 20 | height: variant === 'square' ? height : width, 21 | borderRadius: variant === 'square' ? Theme.spacer.spacing2 : '50%', 22 | 23 | background: `linear-gradient(-90deg,${Theme.color.gray100}, ${Theme.color.gray200}, ${Theme.color.gray100}, ${Theme.color.gray200})`, 24 | backgroundSize: ' 400%', 25 | 26 | animation: `${skeletonAnimation} 5s infinite ease-out`, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { getSkeletonStyling } from '@components/Skeleton/Skeleton.style'; 4 | 5 | export interface SkeletonProps extends ComponentPropsWithoutRef<'div'> { 6 | width?: string; 7 | height?: string; 8 | /** 9 | * Skeleton 모양 10 | * 11 | * @default 'square' 12 | */ 13 | variant?: 'square' | 'circle'; 14 | } 15 | 16 | const Skeleton = ({ 17 | width = '100%', 18 | height = '24px', 19 | variant = 'square', 20 | className = '', 21 | ...attributes 22 | }: SkeletonProps) => { 23 | return ( 24 |
    29 | ); 30 | }; 31 | 32 | export default Skeleton; 33 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { SpinnerProps } from '@components/Spinner/Spinner'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | import { spinnerRotation } from '@styles/animation'; 7 | 8 | export const getSpinnerStyling = ({ timing, size, width, disabled }: Required) => { 9 | return css({ 10 | display: 'inline-block', 11 | 12 | width: `${size}px`, 13 | height: `${size}px`, 14 | border: `${width}px solid ${Theme.color.gray200}`, 15 | borderBottomColor: disabled ? Theme.color.gray600 : Theme.color.blue500, 16 | borderRadius: '50%', 17 | 18 | animation: `${spinnerRotation} ${timing}s linear infinite`, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { getSpinnerStyling } from './Spinner.style'; 2 | 3 | export interface SpinnerProps { 4 | timing?: number; 5 | size?: number; 6 | width?: number; 7 | disabled?: boolean; 8 | } 9 | 10 | const Spinner = ({ timing = 1, size = 50, width = 5, disabled = false }: SpinnerProps) => { 11 | return
    ; 12 | }; 13 | 14 | export default Spinner; 15 | -------------------------------------------------------------------------------- /src/components/StarRatingInput/StarRatingInput.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const inputContainerStyling = (size: number, gap: number) => { 4 | return css({ 5 | display: 'flex', 6 | 7 | 'div:not(:last-of-type):nth-of-type(even)': { 8 | paddingRight: gap, 9 | }, 10 | svg: { 11 | width: size / 2, 12 | height: size, 13 | }, 14 | }); 15 | }; 16 | 17 | export const starItemStyling = css({ 18 | cursor: 'pointer', 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/SupportingText/SupportingText.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { SupportingTextProps } from '@components/SupportingText/SupportingText'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getTextStyling = (isError: Required['isError']) => 8 | css({ 9 | fontSize: Theme.text.small.fontSize, 10 | lineHeight: Theme.text.small.lineHeight, 11 | color: isError ? Theme.color.red300 : Theme.color.gray600, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/SupportingText/SupportingText.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { getTextStyling } from '@components/SupportingText/SupportingText.style'; 4 | 5 | export interface SupportingTextProps extends ComponentPropsWithoutRef<'span'> { 6 | isError?: boolean; 7 | } 8 | 9 | const SupportingText = ({ isError = false, children, ...attributes }: SupportingTextProps) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default SupportingText; 18 | -------------------------------------------------------------------------------- /src/components/SwitchToggle/SwitchToggle.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const switchToggleStyling = css({ 6 | appearance: 'none', 7 | position: 'relative', 8 | width: '30px', 9 | height: '16px', 10 | 11 | boxSizing: 'content-box', 12 | backgroundColor: Theme.color.gray300, 13 | border: `4px solid ${Theme.color.gray300}`, 14 | borderColor: Theme.color.gray300, 15 | borderRadius: '16px', 16 | 17 | cursor: 'pointer', 18 | 19 | '&::before': { 20 | position: 'absolute', 21 | left: 0, 22 | width: '16px', 23 | height: '16px', 24 | 25 | backgroundColor: Theme.color.white, 26 | borderRadius: '50%', 27 | 28 | content: '""', 29 | transition: 'left 250ms linear', 30 | }, 31 | 32 | '&:checked': { 33 | backgroundColor: Theme.color.blue600, 34 | border: `4px solid ${Theme.color.blue600}`, 35 | }, 36 | 37 | '&:checked::before': { 38 | left: '14px', 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/SwitchToggle/SwitchToggle.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent, ComponentPropsWithoutRef } from 'react'; 2 | 3 | import { switchToggleStyling } from './SwitchToggle.style'; 4 | 5 | export interface SwitchToggleProps extends ComponentPropsWithoutRef<'input'> { 6 | onChange: (e: ChangeEvent) => void; 7 | checkedState: boolean; 8 | } 9 | 10 | const SwitchToggle = ({ onChange, checkedState, ...attributes }: SwitchToggleProps) => { 11 | return ( 12 | 21 | ); 22 | }; 23 | 24 | export default SwitchToggle; 25 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export type TabSelectedStylingProps = 'outline' | 'block'; 6 | 7 | export const tabStyling = css({ 8 | display: 'flex', 9 | alignItems: 'center', 10 | justifyContent: 'center', 11 | 12 | minWidth: 'max-content', 13 | padding: '12px 16px', 14 | 15 | fontSize: Theme.text.medium.fontSize, 16 | lineHeight: Theme.text.medium.lineHeight, 17 | 18 | transition: 'all .2s ease-in', 19 | 20 | cursor: 'pointer', 21 | }); 22 | 23 | export const getVariantStyling = (variant: TabSelectedStylingProps, isSelected: boolean) => { 24 | const style = { 25 | outline: css({ 26 | borderBottom: `1px solid ${isSelected ? Theme.color.blue600 : 'transparent'}`, 27 | 28 | backgroundColor: Theme.color.white, 29 | 30 | color: isSelected ? Theme.color.blue600 : Theme.color.gray500, 31 | 32 | '&:hover': { 33 | color: isSelected ? Theme.color.blue600 : Theme.color.gray600, 34 | }, 35 | }), 36 | 37 | block: css({ 38 | border: 'none', 39 | 40 | backgroundColor: isSelected ? Theme.color.blue600 : Theme.color.gray100, 41 | 42 | color: isSelected ? Theme.color.white : Theme.color.gray500, 43 | 44 | '&:hover': { 45 | color: isSelected ? Theme.color.white : Theme.color.gray600, 46 | }, 47 | }), 48 | }; 49 | 50 | return style[variant]; 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/Tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithRef, ForwardedRef, KeyboardEvent } from 'react'; 2 | import { forwardRef } from 'react'; 3 | 4 | import type { TabSelectedStylingProps } from '@components/Tab/Tab.style'; 5 | import { getVariantStyling, tabStyling } from '@components/Tab/Tab.style'; 6 | 7 | export interface TabProps extends ComponentPropsWithRef<'li'> { 8 | variant?: TabSelectedStylingProps; 9 | tabId: string | number; 10 | selectedId: string | number; 11 | text: string; 12 | changeSelect: (tabId: string | number) => void; 13 | } 14 | 15 | const Tab = ( 16 | { tabId, selectedId, variant = 'outline', text, changeSelect, ...attributes }: TabProps, 17 | ref?: ForwardedRef 18 | ) => { 19 | const handleEnterKeyPress = (event: KeyboardEvent) => { 20 | if (event.key === 'Enter') { 21 | changeSelect(tabId); 22 | } 23 | }; 24 | 25 | return ( 26 |
  • { 32 | changeSelect(tabId); 33 | }} 34 | onKeyDown={handleEnterKeyPress} 35 | {...attributes} 36 | > 37 | {text} 38 |
  • 39 | ); 40 | }; 41 | 42 | export default forwardRef(Tab); 43 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import { Theme } from '@styles/Theme'; 4 | 5 | export const containerStyling = css({ 6 | display: 'flex', 7 | 8 | width: 'max-content', 9 | borderBottom: `1px solid ${Theme.color.gray200}`, 10 | 11 | overflowX: 'scroll', 12 | msOverflowStyle: 'none', 13 | scrollbarWidth: 'none', 14 | 15 | '&::-webkit-scrollbar': { 16 | display: 'none', 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 2 | import { forwardRef } from 'react'; 3 | 4 | import { containerStyling } from '@components/Tabs/Tabs.style'; 5 | 6 | export type TabsProps = ComponentPropsWithRef<'ul'>; 7 | 8 | const Tabs = ({ children, ...attributes }: TabsProps, ref: ForwardedRef) => { 9 | return ( 10 |
      11 | {children} 12 |
    13 | ); 14 | }; 15 | 16 | export default forwardRef(Tabs); 17 | -------------------------------------------------------------------------------- /src/components/Text/Text.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { TextProps } from '@components/Text/Text'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const getSizeStyling = (size: Required['size']) => { 8 | const style = { 9 | large: css({ 10 | fontSize: Theme.text.large.fontSize, 11 | lineHeight: Theme.text.large.lineHeight, 12 | }), 13 | medium: css({ 14 | fontSize: Theme.text.medium.fontSize, 15 | lineHeight: Theme.text.medium.lineHeight, 16 | }), 17 | small: css({ 18 | fontSize: Theme.text.small.fontSize, 19 | lineHeight: Theme.text.small.lineHeight, 20 | }), 21 | xSmall: css({ 22 | fontSize: Theme.text.xSmall.fontSize, 23 | lineHeight: Theme.text.xSmall.lineHeight, 24 | }), 25 | }; 26 | 27 | return style[size]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Text/Text.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithoutRef } from 'react'; 3 | 4 | import * as S from '@components/Text/Text.style'; 5 | 6 | export interface TextProps extends ComponentPropsWithoutRef<'p'> { 7 | size?: Extract; 8 | } 9 | 10 | const Text = ({ size = 'medium', children, ...attributes }: TextProps) => { 11 | return ( 12 |

    13 | {children} 14 |

    15 | ); 16 | }; 17 | 18 | export default Text; 19 | -------------------------------------------------------------------------------- /src/components/Textarea/Textarea.style.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | import type { TextareaProps } from '@components/Textarea/Textarea'; 4 | 5 | import { Theme } from '@styles/Theme'; 6 | 7 | export const textareaContainerStyling = css({ 8 | display: 'flex', 9 | flexDirection: 'column', 10 | gap: Theme.spacer.spacing2, 11 | }); 12 | 13 | export const getSizeStyling = (size: Required['size']) => { 14 | const style = { 15 | large: css({ 16 | padding: '14px 16px', 17 | 18 | fontSize: Theme.text.medium.fontSize, 19 | lineHeight: Theme.text.medium.lineHeight, 20 | }), 21 | 22 | medium: css({ 23 | padding: '12px 16px', 24 | 25 | fontSize: Theme.text.medium.fontSize, 26 | lineHeight: Theme.text.medium.lineHeight, 27 | }), 28 | 29 | small: css({ 30 | padding: '8px 12px', 31 | 32 | fontSize: Theme.text.small.fontSize, 33 | lineHeight: Theme.text.small.lineHeight, 34 | }), 35 | }; 36 | 37 | return style[size]; 38 | }; 39 | 40 | export const getTextareaStyling = (isError: Required['isError']) => { 41 | return css({ 42 | width: '100%', 43 | padding: 0, 44 | border: 'none', 45 | borderRadius: Theme.borderRadius.small, 46 | outline: 0, 47 | 48 | backgroundColor: isError ? `${Theme.color.red100}` : Theme.color.gray100, 49 | 50 | transition: 'all .2s ease-in', 51 | 52 | '&:focus-within': { 53 | backgroundColor: isError ? Theme.color.red100 : Theme.color.white, 54 | boxShadow: isError ? `0 0 0 1px ${Theme.color.red200}` : `0 0 0 1px ${Theme.color.gray300}`, 55 | }, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/Textarea/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import type { Size } from '@type/index'; 2 | import type { ComponentPropsWithRef, ForwardedRef } from 'react'; 3 | import { forwardRef } from 'react'; 4 | 5 | import Label from '@components/Label/Label'; 6 | import SupportingText from '@components/SupportingText/SupportingText'; 7 | import { 8 | getSizeStyling, 9 | getTextareaStyling, 10 | textareaContainerStyling, 11 | } from '@components/Textarea/Textarea.style'; 12 | 13 | export interface TextareaProps extends Omit, 'size'> { 14 | label?: string; 15 | size?: Extract; 16 | isError?: boolean; 17 | supportingText?: string; 18 | } 19 | 20 | const Textarea = ( 21 | { label, size = 'medium', isError = false, supportingText, ...attributes }: TextareaProps, 22 | ref: ForwardedRef 23 | ) => { 24 | return ( 25 |
    26 | {label && ( 27 | 30 | )} 31 |