├── .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 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/check-circle-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/checked-icon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/svg/close-icon copy.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/close-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/empty-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/filled-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/half-empty-left-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/half-empty-right-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/half-filled-left-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/half-filled-right-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/image-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/left-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/right-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/search-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/unchecked-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
82 |
83 |
84 |
90 |
91 |
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 | 0,
56 | imageUrls?.length === maxUploadCount
57 | )}
58 | type="button"
59 | onClick={handleImageUploadButton}
60 | >
61 |
62 | {(imageUrls === null || imageUrls.length === 0) && '이미지를 업로드해 주세요'}
63 |
64 |
73 | {imageUrls &&
74 | imageUrls.map((imageUrl) => (
75 |
76 |
77 |
83 |
84 |
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 |
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 |
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 |
82 | ))}
83 |
84 | {supportingText &&
{supportingText}}
85 |
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 |
74 |
75 |
76 |
81 |
82 |
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 |
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 |
36 | {supportingText && {supportingText}}
37 |
38 | );
39 | };
40 |
41 | export default forwardRef(Textarea);
42 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.style.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | import type { ToastProps } from '@components/Toast/Toast';
4 |
5 | import { Theme } from '@styles/Theme';
6 | import { fadeIn, fadeOut, moveUp } from '@styles/animation';
7 |
8 | export const getVariantStyling = (variant: Required['variant']) => {
9 | const style = {
10 | default: css({
11 | backgroundColor: Theme.color.blue600,
12 | }),
13 | success: css({
14 | backgroundColor: Theme.color.green,
15 | }),
16 | error: css({
17 | backgroundColor: Theme.color.red300,
18 | }),
19 | };
20 |
21 | return style[variant];
22 | };
23 |
24 | export const getToastStyling = (isVisible: boolean) => {
25 | return css({
26 | bottom: Theme.spacer.spacing6,
27 | display: 'flex',
28 | justifyContent: 'space-between',
29 | gap: Theme.spacer.spacing4,
30 | alignItems: 'center',
31 |
32 | minWidth: '300px',
33 | padding: `14px ${Theme.spacer.spacing3}`,
34 | borderRadius: Theme.borderRadius.small,
35 |
36 | boxShadow: Theme.boxShadow.shadow9,
37 |
38 | color: Theme.color.white,
39 |
40 | animation: isVisible
41 | ? `${fadeIn} 0.2s ease-in, ${moveUp} 0.2s ease-in`
42 | : `${fadeOut} 0.2s ease-in forwards`,
43 |
44 | '& > svg': {
45 | width: '16px',
46 | height: '16px',
47 |
48 | '& path': {
49 | stroke: Theme.color.white,
50 | },
51 | },
52 | });
53 | };
54 |
55 | export const contentStyling = css({
56 | display: 'flex',
57 | gap: Theme.spacer.spacing2,
58 | alignItems: 'center',
59 |
60 | fontSize: Theme.text.medium.fontSize,
61 | lineHeight: Theme.text.medium.lineHeight,
62 | });
63 |
64 | export const closeIconStyling = css({
65 | cursor: 'pointer',
66 | });
67 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.tsx:
--------------------------------------------------------------------------------
1 | import CloseIcon from '@assets/svg/close-icon.svg';
2 | import { TOAST_CLOSE_ANIMATION_DURATION, TOAST_SHOW_DURATION } from '@constants/index';
3 | import type { ComponentPropsWithoutRef } from 'react';
4 | import { useCallback, useEffect, useRef, useState } from 'react';
5 | import { createPortal } from 'react-dom';
6 |
7 | import {
8 | closeIconStyling,
9 | contentStyling,
10 | getToastStyling,
11 | getVariantStyling,
12 | } from '@components/Toast/Toast.style';
13 |
14 | export interface ToastProps extends ComponentPropsWithoutRef<'div'> {
15 | /**
16 | * Toast의 비주얼 스타일 - 디폴트, 성공, 에러
17 | *
18 | * @default 'default'
19 | */
20 | variant?: 'default' | 'success' | 'error';
21 | /**
22 | * Toast를 닫을 수 있는지에 대한 여부
23 | *
24 | * @default false
25 | */
26 | hasCloseButton?: boolean;
27 | /**
28 | * Toast를 보여주는 시간
29 | *
30 | * @default 2000
31 | */
32 | showDuration?: number;
33 | /** Toast를 닫을 때 실행할 함수 */
34 | onClose: () => void;
35 | }
36 |
37 | const Toast = ({
38 | variant = 'default',
39 | hasCloseButton = false,
40 | showDuration = TOAST_SHOW_DURATION,
41 | onClose,
42 | children,
43 | ...attributes
44 | }: ToastProps) => {
45 | const [isAdded, setIsAdded] = useState(true);
46 | const [isVisible, setIsVisible] = useState(true);
47 |
48 | const showAnimationRef = useRef();
49 | const hideAnimationRef = useRef();
50 |
51 | const handleClose = useCallback(() => {
52 | hideAnimationRef.current = setTimeout(() => {
53 | setIsAdded(false);
54 | onClose?.();
55 | clearTimeout(showAnimationRef.current);
56 | }, TOAST_CLOSE_ANIMATION_DURATION);
57 | }, [onClose]);
58 |
59 | useEffect(() => {
60 | showAnimationRef.current = setTimeout(() => {
61 | setIsVisible(false);
62 | handleClose();
63 | }, showDuration);
64 |
65 | return () => clearTimeout(hideAnimationRef.current);
66 | }, [handleClose, showDuration]);
67 |
68 | return (
69 | isAdded &&
70 | createPortal(
71 |
77 | {children}
78 | {hasCloseButton && }
79 |
,
80 | document.getElementById('toast-container') as Element
81 | )
82 | );
83 | };
84 |
85 | export default Toast;
86 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/ToastContainer.style.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | export const containerStyling = css({
4 | position: 'fixed',
5 | bottom: '24px',
6 | display: 'flex',
7 | flexDirection: 'column',
8 | gap: '16px',
9 | justifyContent: 'center',
10 | alignItems: 'center',
11 |
12 | width: '100%',
13 |
14 | zIndex: 9999,
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/ToastContainer.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyling } from '@components/ToastContainer/ToastContainer.style';
2 |
3 | /** Toast 컴포넌트들이 쌓이는 컨테이너 */
4 | const ToastContainer = () => {
5 | return ;
6 | };
7 |
8 | export default ToastContainer;
9 |
--------------------------------------------------------------------------------
/src/components/Toggle/Toggle.style.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | import { Theme } from '@styles/Theme';
4 |
5 | export const getToggleStyling = (isSelected: boolean) => {
6 | return css({
7 | display: 'flex',
8 | alignItems: 'center',
9 | justifyContent: 'center',
10 |
11 | padding: '8px 12px',
12 | border: `1px solid ${isSelected ? Theme.color.blue700 : Theme.color.gray200}`,
13 |
14 | backgroundColor: isSelected ? Theme.color.blue100 : Theme.color.white,
15 |
16 | fontSize: Theme.text.small.fontSize,
17 | lineHeight: Theme.text.small.lineHeight,
18 | color: isSelected ? Theme.color.blue700 : Theme.color.gray600,
19 |
20 | transition: `all .2s ease-in`,
21 |
22 | cursor: 'pointer',
23 |
24 | '&:hover': {
25 | color: isSelected ? Theme.color.blue700 : Theme.color.gray700,
26 | backgroundColor: isSelected ? Theme.color.blue200 : Theme.color.gray100,
27 | },
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/Toggle/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithRef, ForwardedRef, KeyboardEvent } from 'react';
2 | import { forwardRef } from 'react';
3 |
4 | import { getToggleStyling } from '@components/Toggle/Toggle.style';
5 |
6 | export interface ToggleProps extends ComponentPropsWithRef<'li'> {
7 | toggleId: string | number;
8 | selectedId: string | number;
9 | text: string;
10 | changeSelect: (toggleId: string | number) => void;
11 | }
12 |
13 | const Toggle = (
14 | { toggleId, text, selectedId, changeSelect, ...attributes }: ToggleProps,
15 | ref?: ForwardedRef
16 | ) => {
17 | const handleEnterKeyPress = (event: KeyboardEvent) => {
18 | if (event.key === 'Enter') {
19 | changeSelect(toggleId);
20 | }
21 | };
22 |
23 | return (
24 | changeSelect(toggleId)}
32 | onKeyDown={handleEnterKeyPress}
33 | {...attributes}
34 | >
35 | {text}
36 |
37 | );
38 | };
39 |
40 | export default forwardRef(Toggle);
41 |
--------------------------------------------------------------------------------
/src/components/ToggleGroup/ToggleGroup.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: 'fit-content',
9 | borderRadius: Theme.borderRadius.small,
10 |
11 | overflow: 'hidden',
12 |
13 | '& :first-of-type': {
14 | borderTopLeftRadius: Theme.borderRadius.small,
15 | borderBottomLeftRadius: Theme.borderRadius.small,
16 | },
17 |
18 | '& :last-of-type': {
19 | borderTopRightRadius: Theme.borderRadius.small,
20 | borderBottomRightRadius: Theme.borderRadius.small,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/ToggleGroup/ToggleGroup.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithRef, ForwardedRef } from 'react';
2 | import { forwardRef } from 'react';
3 |
4 | import { containerStyling } from '@components/ToggleGroup/ToggleGroup.style';
5 |
6 | export interface ToggleGroupProps extends ComponentPropsWithRef<'ul'> {}
7 |
8 | const ToggleGroup = (
9 | { children, ...attributes }: ToggleGroupProps,
10 | ref: ForwardedRef
11 | ) => {
12 | return (
13 |
16 | );
17 | };
18 |
19 | export default forwardRef(ToggleGroup);
20 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const DAYS_OF_WEEK = ['일', '월', '화', '수', '목', '금', '토'] as const;
2 |
3 | export const CALENDAR_DATE_LENGTH = {
4 | MIN: 35,
5 | MAX: 42,
6 | } as const;
7 |
8 | export const CALENDAR_MONTH_CHANGE = {
9 | NEXT_MONTH: 1,
10 | PREVIOUS_MONTH: -1,
11 | } as const;
12 |
13 | export const DEFAULT_MAX_DATE_RANGE = 60;
14 |
15 | export const TOAST_SHOW_DURATION = 3000;
16 | export const TOAST_CLOSE_ANIMATION_DURATION = 600;
17 |
--------------------------------------------------------------------------------
/src/hooks/useCalendar.ts:
--------------------------------------------------------------------------------
1 | import type { YearMonth } from '@type/date';
2 | import { useState } from 'react';
3 |
4 | import { getNewYearMonthInfo, getYearMonthInfo } from '@utils/date';
5 |
6 | export const useCalendar = () => {
7 | const currentDate = new Date();
8 | const currentYearMonth = getYearMonthInfo(currentDate);
9 |
10 | const [selectedDate, setSelectedDate] = useState(currentDate.getDate());
11 | const [yearMonth, setYearMonth] = useState(currentYearMonth);
12 |
13 | const handleDateClick = (date: number) => () => {
14 | setSelectedDate(date);
15 | };
16 |
17 | const handleYearMonthUpdate = (change: number) => () => {
18 | setSelectedDate(0);
19 | setYearMonth((prev) => getNewYearMonthInfo(prev, change));
20 | };
21 |
22 | return {
23 | currentDate,
24 | yearMonth,
25 | selectedDate,
26 | handleDateClick,
27 | handleYearMonthUpdate,
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/hooks/useCarousel.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import type { MouseEvent } from 'react';
3 | import { flushSync } from 'react-dom';
4 |
5 | const useCarousel = (itemLength: number) => {
6 | const [viewIndex, setViewIndex] = useState(0);
7 | const carouselBoxRef = useRef(null);
8 | const itemRef = useRef(null);
9 |
10 | const handleMoveImage = (imageNumber: number) => {
11 | if (itemRef.current) {
12 | flushSync(() => {
13 | setViewIndex(imageNumber);
14 | });
15 |
16 | itemRef.current.scrollIntoView({
17 | behavior: 'smooth',
18 | block: 'nearest',
19 | inline: 'center',
20 | });
21 | }
22 | };
23 |
24 | const handleClickLeft = (e: MouseEvent) => {
25 | e.stopPropagation();
26 | if (itemRef.current) {
27 | flushSync(() => {
28 | if (viewIndex === 0) setViewIndex(0);
29 | else setViewIndex(viewIndex - 1);
30 | });
31 |
32 | itemRef.current.scrollIntoView({
33 | behavior: 'smooth',
34 | block: 'nearest',
35 | inline: 'center',
36 | });
37 | }
38 | };
39 |
40 | const handleClickRight = (e: MouseEvent) => {
41 | e.stopPropagation();
42 | if (itemRef.current) {
43 | flushSync(() => {
44 | if (viewIndex === itemLength - 1) setViewIndex(viewIndex);
45 | else setViewIndex(viewIndex + 1);
46 | });
47 |
48 | itemRef.current.scrollIntoView({
49 | behavior: 'smooth',
50 | block: 'nearest',
51 | inline: 'center',
52 | });
53 | }
54 | };
55 |
56 | return {
57 | viewIndex,
58 | itemRef,
59 | carouselBoxRef,
60 | handleMoveImage,
61 | handleClickLeft,
62 | handleClickRight,
63 | };
64 | };
65 |
66 | export default useCarousel;
67 |
--------------------------------------------------------------------------------
/src/hooks/useDateRangePicker.ts:
--------------------------------------------------------------------------------
1 | import { CALENDAR_MONTH_CHANGE } from '@constants/index';
2 | import type { DateRangePickerCalendar, SelectedDateRange } from '@type/date';
3 | import { useState } from 'react';
4 |
5 | import { getNewYearMonthInfo, getYearMonthInfo, toDate } from '@utils/date';
6 |
7 | export const useDateRangePicker = (initialSelectedDateRange?: SelectedDateRange) => {
8 | const currentDate = new Date();
9 | const initialDate = initialSelectedDateRange
10 | ? toDate(initialSelectedDateRange.startDate!)
11 | : currentDate;
12 | const currentMonthYearDetail = getYearMonthInfo(initialDate);
13 |
14 | const [calendarData, setCalendarData] = useState({
15 | prevYearMonth: getNewYearMonthInfo(
16 | currentMonthYearDetail,
17 | CALENDAR_MONTH_CHANGE.PREVIOUS_MONTH
18 | ),
19 | currentYearMonth: currentMonthYearDetail,
20 | });
21 |
22 | const [selectedDateRange, setSelectedDateRange] = useState(
23 | initialSelectedDateRange ?? {
24 | startDate: null,
25 | endDate: null,
26 | }
27 | );
28 |
29 | const handleMonthChange = (change: number) => () => {
30 | setCalendarData((prevCalendarData) => {
31 | const newCalendarData = { ...prevCalendarData };
32 |
33 | if (change > 0) {
34 | newCalendarData.prevYearMonth = prevCalendarData.currentYearMonth;
35 | newCalendarData.currentYearMonth = getNewYearMonthInfo(
36 | newCalendarData.prevYearMonth,
37 | change
38 | );
39 | }
40 |
41 | if (change < 0) {
42 | newCalendarData.currentYearMonth = prevCalendarData.prevYearMonth;
43 | newCalendarData.prevYearMonth = getNewYearMonthInfo(
44 | newCalendarData.currentYearMonth,
45 | change
46 | );
47 | }
48 |
49 | return newCalendarData;
50 | });
51 | };
52 |
53 | const resetSelectedDateRange = () => {
54 | setSelectedDateRange({ startDate: null, endDate: null });
55 | };
56 |
57 | const handleDateSelect = (dateString: string, onDaySelect?: CallableFunction) => {
58 | setSelectedDateRange((prevSelectedDateRange) => {
59 | const startDate = prevSelectedDateRange.startDate
60 | ? new Date(prevSelectedDateRange.startDate)
61 | : null;
62 | const selectedDate = new Date(dateString);
63 |
64 | const nextSelectedDates: SelectedDateRange = {
65 | startDate: null,
66 | endDate: null,
67 | };
68 |
69 | if (startDate && !prevSelectedDateRange.endDate && selectedDate < startDate) {
70 | nextSelectedDates.startDate = dateString;
71 | nextSelectedDates.endDate = prevSelectedDateRange.startDate;
72 | } else if (startDate && !prevSelectedDateRange.endDate) {
73 | nextSelectedDates.startDate = prevSelectedDateRange.startDate;
74 | nextSelectedDates.endDate = dateString;
75 | } else {
76 | nextSelectedDates.startDate = dateString;
77 | }
78 |
79 | onDaySelect?.(nextSelectedDates);
80 |
81 | return nextSelectedDates;
82 | });
83 | };
84 |
85 | return {
86 | currentDate,
87 | calendarData,
88 | handleMonthChange,
89 | selectedDateRange,
90 | resetSelectedDateRange,
91 | handleDateSelect,
92 | };
93 | };
94 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useDebounce = (value: T, delay: number = 500): T => {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const timer = setTimeout(() => setDebouncedValue(value), delay);
8 |
9 | return () => {
10 | clearTimeout(timer);
11 | };
12 | }, [value, delay]);
13 |
14 | return debouncedValue;
15 | };
16 |
--------------------------------------------------------------------------------
/src/hooks/useImageCarousel.ts:
--------------------------------------------------------------------------------
1 | import type { KeyboardEvent, MouseEvent, TouchEvent } from 'react';
2 | import { useRef, useState } from 'react';
3 |
4 | import { limitToRange } from '@utils/number';
5 |
6 | export const useImageCarousel = (width: number, slideLength: number) => {
7 | const [currentPosition, setCurrentPosition] = useState(0);
8 | const [translateX, setTranslateX] = useState(0);
9 | const [animate, setAnimate] = useState(false);
10 | const sliderRef = useRef(null);
11 |
12 | const handleDragChange = (offsetX: number) => {
13 | setTranslateX(limitToRange(offsetX, -width, width));
14 | };
15 |
16 | const handleDragEnd = (offsetX: number) => {
17 | const maxPosition = slideLength - 1;
18 |
19 | if (offsetX < -50) setCurrentPosition(limitToRange(currentPosition + 1, 0, maxPosition));
20 | if (offsetX > 50) setCurrentPosition(limitToRange(currentPosition - 1, 0, maxPosition));
21 |
22 | setAnimate(true);
23 | setTranslateX(0);
24 | };
25 |
26 | const handleSliderNavigationClick =
27 | (position: number) => (event: MouseEvent) => {
28 | event.stopPropagation();
29 |
30 | if (position < 0 || position >= slideLength) return;
31 |
32 | setCurrentPosition(position);
33 | setAnimate(true);
34 | setTranslateX(0);
35 | };
36 |
37 | const handleSliderNavigationEnterKeyPress =
38 | (position: number) => (event: KeyboardEvent) => {
39 | if (event.key === 'Enter') {
40 | handleSliderNavigationClick(position);
41 | }
42 | };
43 |
44 | const handlerSliderMoueDown = (clickEvent: MouseEvent) => {
45 | const handleMouseMove = (moveEvent: globalThis.MouseEvent) => {
46 | const offsetX = moveEvent.pageX - clickEvent.pageX;
47 | handleDragChange(offsetX);
48 | };
49 |
50 | const handleMouseUp = (moveEvent: globalThis.MouseEvent) => {
51 | const offsetX = moveEvent.pageX - clickEvent.pageX;
52 | handleDragEnd(offsetX);
53 |
54 | sliderRef.current?.removeEventListener('mousemove', handleMouseMove);
55 | };
56 |
57 | sliderRef.current?.addEventListener('mousemove', handleMouseMove);
58 | sliderRef.current?.addEventListener('mouseup', handleMouseUp, { once: true });
59 | };
60 |
61 | const handleSliderTouchStart = (touchEvent: TouchEvent) => {
62 | const handleTouchMove = (moveEvent: globalThis.TouchEvent) => {
63 | /** 모바일에서 드래그할 떄 스크롤하지 못하도록 막아준다 */
64 | if (moveEvent.cancelable) moveEvent.preventDefault();
65 |
66 | const offset = moveEvent.touches[0].pageX - touchEvent.touches[0].pageX;
67 | handleDragChange(offset);
68 | };
69 |
70 | const handleTouchEnd = (moveEvent: globalThis.TouchEvent) => {
71 | const offset = moveEvent.changedTouches[0].pageX - touchEvent.changedTouches[0].pageX;
72 | handleDragEnd(offset);
73 |
74 | sliderRef.current?.removeEventListener('touchmove', handleTouchMove);
75 | };
76 |
77 | sliderRef.current?.addEventListener('touchmove', handleTouchMove, { passive: false });
78 | sliderRef.current?.addEventListener('touchend', handleTouchEnd, { once: true });
79 | };
80 |
81 | const handleSliderTransitionEnd = () => {
82 | setAnimate(false);
83 | };
84 |
85 | return {
86 | sliderRef,
87 | animate,
88 | currentPosition,
89 | translateX,
90 | handleSliderNavigationClick,
91 | handleSliderNavigationEnterKeyPress,
92 | handlerSliderMoueDown,
93 | handleSliderTouchStart,
94 | handleSliderTransitionEnd,
95 | };
96 | };
97 |
--------------------------------------------------------------------------------
/src/hooks/useOverlay.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export const useOverlay = () => {
4 | const [isOpen, setIsOpen] = useState(false);
5 |
6 | const open = useCallback(() => {
7 | setIsOpen(true);
8 | }, [setIsOpen]);
9 |
10 | const close = useCallback(() => {
11 | setIsOpen(false);
12 | }, [setIsOpen]);
13 |
14 | const toggle = useCallback(() => {
15 | setIsOpen((prev) => !prev);
16 | }, []);
17 |
18 | return { isOpen, open, close, toggle };
19 | };
20 |
--------------------------------------------------------------------------------
/src/hooks/useSelect.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export const useSelect = (initialSelectedId: number | string) => {
4 | const [selected, setSelected] = useState(initialSelectedId);
5 |
6 | const handleSelectClick = useCallback((selectedId: number | string) => {
7 | setSelected(selectedId);
8 | }, []);
9 |
10 | return { selected, handleSelectClick };
11 | };
12 |
--------------------------------------------------------------------------------
/src/hooks/useStarRatingInput.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef, useState } from 'react';
2 |
3 | import { useDebounce } from '@hooks/useDebounce';
4 |
5 | type InitialRateType = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 4.5 | 5;
6 |
7 | export const useStarRatingInput = (initialRate: InitialRateType, onClick?: CallableFunction) => {
8 | const [tempStarRate, setStarRate] = useState(initialRate);
9 | const starRate = useDebounce(tempStarRate, 50);
10 |
11 | const [tempHookStarRate, setHookStarRate] = useState(initialRate);
12 | const hookStarRate = useDebounce(tempHookStarRate, 50);
13 |
14 | const [tempPrevStarRate, setPrevStarRate] = useState(initialRate);
15 | const prevStarRate = useDebounce(tempPrevStarRate, 50);
16 |
17 | const hoverState = useRef(false);
18 |
19 | const handleStarClick = useCallback(
20 | (index: number) => {
21 | const newRate = ((index + 1) / 2) as InitialRateType;
22 |
23 | if (hookStarRate === newRate) {
24 | setStarRate(0);
25 | setPrevStarRate(0);
26 | setHookStarRate(0);
27 | onClick?.(0);
28 | } else {
29 | setStarRate(newRate);
30 | setHookStarRate(newRate);
31 | setPrevStarRate(newRate);
32 | onClick?.(newRate);
33 | }
34 | },
35 | [hookStarRate, onClick]
36 | );
37 |
38 | const handleStarHover = useCallback((index: number) => {
39 | const newRate = ((index + 1) / 2) as InitialRateType;
40 |
41 | setStarRate(newRate);
42 | hoverState.current = true;
43 | }, []);
44 |
45 | const handleStarHoverOut = useCallback(() => {
46 | if (hoverState.current) {
47 | setStarRate(prevStarRate as InitialRateType);
48 | }
49 |
50 | hoverState.current = false;
51 | }, [prevStarRate]);
52 |
53 | return {
54 | starRate,
55 | handleStarClick,
56 | handleStarHover,
57 | handleStarHoverOut,
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import HangLogProvider from '@/HangLogProvider';
2 |
3 | import { useCalendar } from '@hooks/useCalendar';
4 | import { useDateRangePicker } from '@hooks/useDateRangePicker';
5 | import { useOverlay } from '@hooks/useOverlay';
6 | import { useSelect } from '@hooks/useSelect';
7 | import { useStarRatingInput } from '@hooks/useStarRatingInput';
8 |
9 | import Badge from '@components/Badge/Badge';
10 | import Box from '@components/Box/Box';
11 | import Button from '@components/Button/Button';
12 | import Calendar from '@components/Calendar/Calendar';
13 | import Carousel from '@components/Carousel/Carousel';
14 | import Center from '@components/Center/Center';
15 | import Checkbox from '@components/Checkbox/Checkbox';
16 | import DateRangePicker from '@components/DateRangePicker/DateRangePicker';
17 | import Divider from '@components/Divider/Divider';
18 | import Flex from '@components/Flex/Flex';
19 | import FloatingButton from '@components/FloatingButton/FloatingButton';
20 | import GeneralCarousel from '@components/GeneralCarousel/Carousel';
21 | import Heading from '@components/Heading/Heading';
22 | import ImageCarousel from '@components/ImageCarousel/ImageCarousel';
23 | import ImageUploadInput from '@components/ImageUploadInput/ImageUploadInput';
24 | import Input from '@components/Input/Input';
25 | import Label from '@components/Label/Label';
26 | import Menu from '@components/Menu/Menu';
27 | import MenuItem from '@components/MenuItem/MenuItem';
28 | import MenuList from '@components/MenuList/MenuList';
29 | import Modal from '@components/Modal/Modal';
30 | import RadioButton from '@components/RadioButton/RadioButton';
31 | import SVGCarousel from '@components/SVGCarousel/SVGCarousel';
32 | import SVGCarouselModal from '@components/SVGCarouselModal/SVGCarouselModal';
33 | import Select from '@components/Select/Select';
34 | import Skeleton from '@components/Skeleton/Skeleton';
35 | import Spinner from '@components/Spinner/Spinner';
36 | import StarRatingInput from '@components/StarRatingInput/StarRatingInput';
37 | import SupportingText from '@components/SupportingText/SupportingText';
38 | import SwitchToggle from '@components/SwitchToggle/SwitchToggle';
39 | import Tab from '@components/Tab/Tab';
40 | import Tabs from '@components/Tabs/Tabs';
41 | import Text from '@components/Text/Text';
42 | import Textarea from '@components/Textarea/Textarea';
43 | import Toast from '@components/Toast/Toast';
44 | import Toggle from '@components/Toggle/Toggle';
45 | import ToggleGroup from '@components/ToggleGroup/ToggleGroup';
46 |
47 | import { Theme } from '@styles/Theme';
48 |
49 | export {
50 | HangLogProvider,
51 | useCalendar,
52 | useDateRangePicker,
53 | useSelect,
54 | useStarRatingInput,
55 | useOverlay,
56 | Badge,
57 | Box,
58 | SwitchToggle,
59 | Button,
60 | Calendar,
61 | Carousel,
62 | Center,
63 | Checkbox,
64 | DateRangePicker,
65 | Divider,
66 | Flex,
67 | FloatingButton,
68 | Heading,
69 | ImageCarousel,
70 | ImageUploadInput,
71 | Input,
72 | Label,
73 | Menu,
74 | MenuItem,
75 | MenuList,
76 | Modal,
77 | RadioButton,
78 | Select,
79 | Skeleton,
80 | Spinner,
81 | StarRatingInput,
82 | SupportingText,
83 | Tab,
84 | Tabs,
85 | Text,
86 | Textarea,
87 | Toast,
88 | Toggle,
89 | ToggleGroup,
90 | SVGCarousel,
91 | SVGCarouselModal,
92 | GeneralCarousel,
93 | Theme,
94 | };
95 |
--------------------------------------------------------------------------------
/src/stories/Badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Badge from '@components/Badge/Badge';
5 |
6 | const meta = {
7 | title: 'Badge',
8 | component: Badge,
9 | argTypes: {
10 | variant: {
11 | control: { type: 'radio' },
12 | options: ['default', 'primary', 'outline'],
13 | description: 'Badge의 비주얼 스타일',
14 | },
15 | children: {
16 | control: { type: 'text' },
17 | description: 'Badge에 표시할 내용',
18 | },
19 | },
20 | args: {
21 | variant: 'default',
22 | children: 'Badge',
23 | },
24 | } satisfies Meta;
25 |
26 | export default meta;
27 | type Story = StoryObj;
28 |
29 | export const Playground: Story = {};
30 |
31 | export const Variants: Story = {
32 | render: ({ children }) => {
33 | return (
34 |
35 | -
36 |
Default
37 | {children}
38 |
39 | -
40 |
Primary
41 | {children}
42 |
43 | -
44 |
Outline
45 | {children}
46 |
47 |
48 | );
49 | },
50 | argTypes: {
51 | variant: {
52 | control: false,
53 | },
54 | },
55 | };
56 |
57 | export const Default: Story = {
58 | argTypes: {
59 | variant: {
60 | control: false,
61 | },
62 | },
63 | };
64 |
65 | export const Primary: Story = {
66 | args: {
67 | variant: 'primary',
68 | },
69 | argTypes: {
70 | variant: {
71 | control: false,
72 | },
73 | },
74 | };
75 |
76 | export const Outline: Story = {
77 | args: {
78 | variant: 'outline',
79 | },
80 | argTypes: {
81 | variant: {
82 | control: false,
83 | },
84 | },
85 | };
86 |
--------------------------------------------------------------------------------
/src/stories/Box.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Box from '@components/Box/Box';
5 |
6 | import { Theme } from '@styles/Theme';
7 |
8 | const meta = {
9 | title: 'Box',
10 | component: Box,
11 | decorators: [
12 | (Story) => (
13 |
16 | ),
17 | ],
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | export const Playground: Story = {
24 | render: (args) => {
25 | return (
26 |
27 | box1
28 |
29 | );
30 | },
31 | args: {
32 | styles: {
33 | backgroundColor: Theme.color.blue500,
34 | borderRadius: '5px',
35 | },
36 | },
37 | argTypes: {
38 | styles: {
39 | control: {
40 | type: 'object',
41 | },
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/src/stories/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import type { ButtonProps } from '@components/Button/Button';
5 | import Button from '@components/Button/Button';
6 |
7 | const meta = {
8 | title: 'Button',
9 | component: Button,
10 | argTypes: {
11 | variant: {
12 | control: { type: 'radio' },
13 | options: ['default', 'primary', 'secondary', 'outline', 'text', 'danger'],
14 | },
15 | size: {
16 | control: { type: 'radio' },
17 | options: ['small', 'medium', 'large'],
18 | },
19 | children: {
20 | control: { type: 'text' },
21 | },
22 | },
23 | args: {
24 | variant: 'default',
25 | size: 'medium',
26 | children: 'Button',
27 | },
28 | } satisfies Meta;
29 |
30 | export default meta;
31 | type Story = StoryObj;
32 |
33 | const createButtonStory = (variant: ButtonProps['variant']) => ({
34 | args: {
35 | variant,
36 | },
37 | argTypes: {
38 | variant: {
39 | control: false,
40 | },
41 | },
42 | });
43 |
44 | export const Playground: Story = {};
45 |
46 | export const Variants: Story = {
47 | render: ({ size, children }) => {
48 | return (
49 |
50 | -
51 |
Default
52 |
53 | {children}
54 |
55 |
56 | -
57 |
Primary
58 |
59 | {children}
60 |
61 |
62 | -
63 |
Secondary
64 |
65 | {children}
66 |
67 |
68 | -
69 |
Text
70 |
71 | {children}
72 |
73 |
74 | -
75 |
Outline
76 |
77 | {children}
78 |
79 |
80 | -
81 |
Danger
82 |
83 | {children}
84 |
85 |
86 |
87 | );
88 | },
89 | argTypes: {
90 | variant: {
91 | control: false,
92 | },
93 | },
94 | };
95 |
96 | export const Sizes: Story = {
97 | render: ({ variant, children }) => {
98 | return (
99 |
100 | -
101 |
Small
102 |
103 | {children}
104 |
105 |
106 | -
107 |
Medium
108 |
109 | {children}
110 |
111 |
112 | -
113 |
Large
114 |
115 | {children}
116 |
117 |
118 |
119 | );
120 | },
121 | argTypes: {
122 | size: {
123 | control: false,
124 | },
125 | },
126 | };
127 |
128 | export const Default: Story = createButtonStory('default');
129 |
130 | export const Primary: Story = createButtonStory('primary');
131 |
132 | export const Secondary: Story = createButtonStory('secondary');
133 |
134 | export const Text: Story = createButtonStory('text');
135 |
136 | export const Outline: Story = createButtonStory('outline');
137 |
138 | export const Danger: Story = createButtonStory('danger');
139 |
--------------------------------------------------------------------------------
/src/stories/Calendar.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { useCalendar } from '@hooks/useCalendar';
4 |
5 | import Calendar from '@components/Calendar/Calendar';
6 |
7 | const meta = {
8 | title: 'Calendar',
9 | component: Calendar,
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Default: Story = {
16 | render: () => {
17 | const { currentDate, yearMonth, selectedDate } = useCalendar();
18 |
19 | return (
20 |
21 | );
22 | },
23 | };
24 |
25 | export const Clickable: Story = {
26 | render: () => {
27 | const { currentDate, yearMonth, selectedDate, handleDateClick } = useCalendar();
28 |
29 | return (
30 |
36 | );
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/stories/Center.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Box from '@components/Box/Box';
4 | import Center from '@components/Center/Center';
5 |
6 | import { Theme } from '..';
7 |
8 | const meta = {
9 | title: 'Center',
10 | component: Center,
11 | } satisfies Meta;
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | render: () => {
18 | return (
19 |
26 |
27 |
34 | Centered Box
35 |
36 |
37 |
38 | );
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/stories/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Checkbox from '@components/Checkbox/Checkbox';
5 |
6 | const meta = {
7 | title: 'Checkbox',
8 | component: Checkbox,
9 | argTypes: {
10 | checked: {
11 | control: { type: 'boolean' },
12 | },
13 | label: {
14 | control: { type: 'text' },
15 | },
16 | },
17 | args: {
18 | checked: true,
19 | label: 'Label',
20 | },
21 | } satisfies Meta;
22 |
23 | export default meta;
24 | type Story = StoryObj;
25 |
26 | export const Playground: Story = {};
27 |
28 | export const Checkboxes: Story = {
29 | render: () => {
30 | return (
31 |
32 | -
33 |
Checked
34 |
35 |
36 | -
37 |
Unchecked
38 |
39 |
40 | -
41 |
Checked with Label
42 |
43 |
44 | -
45 |
Unchecked with Label
46 |
47 |
48 |
49 | );
50 | },
51 | argTypes: {
52 | checked: {
53 | control: false,
54 | },
55 | label: {
56 | control: false,
57 | },
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/src/stories/DateRangePicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DEFAULT_MAX_DATE_RANGE } from '@constants/index';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import DateRangePicker from '@components/DateRangePicker/DateRangePicker';
5 |
6 | const meta = {
7 | title: 'DateRangePicker',
8 | component: DateRangePicker,
9 | argTypes: {
10 | onDateSelect: { control: false },
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Default: Story = {};
18 |
19 | export const FutureDaysDisabled: Story = {
20 | args: {
21 | isFutureDaysRestricted: true,
22 | },
23 | };
24 |
25 | export const DaysDisabled: Story = {
26 | args: {
27 | hasRangeRestriction: true,
28 | maxDateRange: DEFAULT_MAX_DATE_RANGE,
29 | },
30 | };
31 |
32 | export const InitialSelectedDateRange = {
33 | args: {
34 | initialSelectedDateRange: {
35 | start: '2023-07-12',
36 | end: '2023-07-30',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/Day.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Day from '@components/Calendar/Day/Day';
5 |
6 | const meta = {
7 | title: 'Day',
8 | component: Day,
9 | argTypes: {
10 | day: {
11 | day: { type: 'number', min: 1, max: 31 },
12 | },
13 | isToday: {
14 | control: { type: 'boolean' },
15 | },
16 | isSelected: {
17 | control: { type: 'boolean' },
18 | },
19 | isInRange: {
20 | control: { type: 'boolean' },
21 | },
22 | isDisabled: {
23 | control: { type: 'boolean' },
24 | },
25 | },
26 | args: {
27 | day: 13,
28 | isToday: false,
29 | isSelected: false,
30 | isInRange: false,
31 | isDisabled: false,
32 | },
33 | } satisfies Meta;
34 |
35 | export default meta;
36 | type Story = StoryObj;
37 |
38 | export const Playground: Story = {};
39 |
40 | export const Variants: Story = {
41 | render: ({ ...args }) => {
42 | return (
43 |
44 | -
45 |
Default
46 |
47 |
48 | -
49 |
Today
50 |
51 |
52 | -
53 |
Selected Day
54 |
55 |
56 | -
57 |
Day in Range
58 |
59 |
60 | -
61 |
Disabled Day
62 |
63 |
64 |
65 | );
66 | },
67 | argTypes: {
68 | isToday: { control: false },
69 | isSelected: { control: false },
70 | isInRange: { control: false },
71 | isDisabled: { control: false },
72 | },
73 | };
74 |
75 | export const Default: Story = {
76 | argTypes: {
77 | isToday: { control: false },
78 | isSelected: { control: false },
79 | isInRange: { control: false },
80 | isDisabled: { control: false },
81 | },
82 | };
83 |
84 | export const Today: Story = {
85 | argTypes: {
86 | isToday: { control: false },
87 | isSelected: { control: false },
88 | isInRange: { control: false },
89 | isDisabled: { control: false },
90 | },
91 | args: {
92 | isToday: true,
93 | },
94 | };
95 |
96 | export const SelectedDay: Story = {
97 | argTypes: {
98 | isToday: { control: false },
99 | isSelected: { control: false },
100 | isInRange: { control: false },
101 | isDisabled: { control: false },
102 | },
103 | args: {
104 | isSelected: true,
105 | },
106 | };
107 |
108 | export const DayInRange: Story = {
109 | argTypes: {
110 | isToday: { control: false },
111 | isSelected: { control: false },
112 | isInRange: { control: false },
113 | isDisabled: { control: false },
114 | },
115 | args: {
116 | isInRange: true,
117 | },
118 | };
119 |
120 | export const DisabledDay: Story = {
121 | argTypes: {
122 | isToday: { control: false },
123 | isSelected: { control: false },
124 | isInRange: { control: false },
125 | isDisabled: { control: false },
126 | },
127 | args: {
128 | isDisabled: true,
129 | },
130 | };
131 |
--------------------------------------------------------------------------------
/src/stories/Divider.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Divider from '@components/Divider/Divider';
5 |
6 | const meta = {
7 | title: 'Divider',
8 | component: Divider,
9 | argTypes: {
10 | direction: {
11 | control: { type: 'radio' },
12 | options: ['horizontal', 'vertical'],
13 | },
14 | length: {
15 | control: { type: 'text' },
16 | },
17 | },
18 | args: {
19 | direction: 'horizontal',
20 | length: '700px',
21 | },
22 | } satisfies Meta;
23 |
24 | export default meta;
25 | type Story = StoryObj;
26 |
27 | export const Playground: Story = {};
28 |
29 | export const Directions: Story = {
30 | render: () => {
31 | return (
32 |
33 | -
34 |
Horizontal
35 |
36 |
37 | -
38 |
Vertical
39 |
40 |
41 |
42 | );
43 | },
44 | argTypes: {
45 | direction: {
46 | control: false,
47 | },
48 | },
49 | };
50 |
51 | export const Horizontal: Story = {
52 | argTypes: {
53 | direction: {
54 | control: false,
55 | },
56 | },
57 | };
58 |
59 | export const Vertical: Story = {
60 | args: {
61 | direction: 'vertical',
62 | },
63 | argTypes: {
64 | direction: {
65 | control: false,
66 | },
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/src/stories/Flex.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Box from '@components/Box/Box';
4 | import Flex from '@components/Flex/Flex';
5 |
6 | import { Theme } from '@styles/Theme';
7 |
8 | const meta = {
9 | title: 'Flex',
10 | component: Flex,
11 | } satisfies Meta;
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | render: (args) => {
18 | return (
19 |
20 |
28 | box1
29 |
30 |
38 | box2
39 |
40 |
48 | box3
49 |
50 |
51 | );
52 | },
53 | args: {
54 | styles: {
55 | width: '800px',
56 | height: '500px',
57 | },
58 | },
59 | argTypes: {
60 | styles: {
61 | control: {
62 | type: 'object',
63 | },
64 | },
65 | },
66 | };
67 |
--------------------------------------------------------------------------------
/src/stories/FloatingButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import type { FloatingButtonProps } from '@components/FloatingButton/FloatingButton';
5 | import FloatingButton from '@components/FloatingButton/FloatingButton';
6 |
7 | const meta = {
8 | title: 'FloatingButton',
9 | component: FloatingButton,
10 | argTypes: {
11 | variant: {
12 | control: { type: 'radio' },
13 | options: ['default', 'primary'],
14 | },
15 | size: {
16 | control: { type: 'radio' },
17 | options: ['small', 'medium'],
18 | },
19 | },
20 | args: {
21 | variant: 'primary',
22 | size: 'medium',
23 | },
24 | } satisfies Meta;
25 |
26 | export default meta;
27 | type Story = StoryObj;
28 |
29 | const createButtonStory = (variant: FloatingButtonProps['variant']) => ({
30 | args: {
31 | variant,
32 | },
33 | argTypes: {
34 | variant: {
35 | control: false,
36 | },
37 | },
38 | });
39 |
40 | export const Playground: Story = {};
41 |
42 | export const Variants: Story = {
43 | render: ({ size }) => {
44 | return (
45 |
46 | -
47 |
Primary
48 |
49 |
50 | -
51 |
Default
52 |
53 |
54 |
55 | );
56 | },
57 | argTypes: {
58 | variant: {
59 | control: false,
60 | },
61 | },
62 | };
63 |
64 | export const Sizes: Story = {
65 | render: ({ variant }) => {
66 | return (
67 |
68 | -
69 |
Small
70 |
71 |
72 | -
73 |
Medium
74 |
75 |
76 |
77 | );
78 | },
79 | argTypes: {
80 | size: {
81 | control: false,
82 | },
83 | },
84 | };
85 |
86 | export const Default: Story = createButtonStory('default');
87 |
88 | export const Primary: Story = createButtonStory('primary');
89 |
--------------------------------------------------------------------------------
/src/stories/GeneralCarousel.stories.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 |
3 | /* eslint-disable jsx-a11y/img-redundant-alt */
4 | import type { Meta, StoryObj } from '@storybook/react';
5 |
6 | import Carousel from '@components/GeneralCarousel/Carousel';
7 |
8 | const meta = {
9 | title: 'Carousel',
10 | component: Carousel,
11 | argTypes: {
12 | width: { control: 'number' },
13 | height: { control: 'number' },
14 | },
15 | args: {
16 | width: 300,
17 | height: 200,
18 | length: 3,
19 | },
20 | } satisfies Meta;
21 |
22 | const images = [
23 | 'https://i.pinimg.com/236x/18/0e/c6/180ec6aaf4b5aab89d91f36752219569.jpg',
24 | 'https://img.freepik.com/free-photo/many-ripe-juicy-red-apples-covered-with-water-drops-closeup-selective-focus-ripe-fruits-as-a-background_166373-2611.jpg?size=626&ext=jpg&ga=GA1.1.1546980028.1703808000&semt=sph',
25 | 'https://img.freepik.com/premium-photo/a-red-apple-with-a-white-background-and-a-white-background_933356-5.jpg',
26 | ];
27 |
28 | export default meta;
29 | type Story = StoryObj;
30 |
31 | export const Default: Story = {
32 | render: ({ ...args }) => {
33 | return (
34 |
35 | {images.map((url, index) => (
36 |
37 |
38 |
39 | ))}
40 |
41 | );
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/stories/Heading.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import type { HeadingProps } from '@components/Heading/Heading';
5 | import Heading from '@components/Heading/Heading';
6 |
7 | const meta = {
8 | title: 'Heading',
9 | component: Heading,
10 | argTypes: {
11 | size: {
12 | control: { type: 'radio' },
13 | options: ['xSmall', 'small', 'medium', 'large', 'xLarge', 'xxLarge'],
14 | },
15 | children: {
16 | control: { type: 'text' },
17 | },
18 | },
19 | args: {
20 | size: 'medium',
21 | children: 'Heading',
22 | },
23 | decorators: [
24 | (Story) => (
25 |
28 | ),
29 | ],
30 | } satisfies Meta;
31 |
32 | export default meta;
33 | type Story = StoryObj;
34 |
35 | const createHeadingStory = (size: HeadingProps['size']) => ({
36 | args: {
37 | size,
38 | },
39 | argTypes: {
40 | size: {
41 | control: false,
42 | },
43 | },
44 | });
45 |
46 | export const Playground: Story = {};
47 |
48 | export const Sizes: Story = {
49 | render: ({ children }) => {
50 | return (
51 | <>
52 |
53 | X Small
54 | {children}
55 |
56 |
57 | Small
58 | {children}
59 |
60 |
61 | Medium
62 | {children}
63 |
64 |
65 | Large
66 | {children}
67 |
68 |
69 | X Large
70 | {children}
71 |
72 |
73 | XX Large
74 | {children}
75 |
76 | >
77 | );
78 | },
79 | argTypes: {
80 | size: {
81 | control: false,
82 | },
83 | },
84 | };
85 |
86 | export const XSmall: Story = createHeadingStory('xSmall');
87 |
88 | export const Small: Story = createHeadingStory('small');
89 |
90 | export const Medium: Story = createHeadingStory('medium');
91 |
92 | export const Large: Story = createHeadingStory('large');
93 |
94 | export const XLarge: Story = createHeadingStory('xLarge');
95 |
96 | export const XXLarge: Story = createHeadingStory('xxLarge');
97 |
--------------------------------------------------------------------------------
/src/stories/ImageCarousel.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import ImageCarousel from '@components/ImageCarousel/ImageCarousel';
4 |
5 | const meta = {
6 | title: 'ImageCarousel',
7 | component: ImageCarousel,
8 | argTypes: {
9 | width: { control: 'number' },
10 | height: { control: 'number' },
11 | showArrows: { control: 'boolean' },
12 | showDots: { control: 'boolean' },
13 | showNavigationOnHover: { control: 'boolean' },
14 | images: { control: false },
15 | },
16 | args: {
17 | width: 250,
18 | height: 167,
19 | showArrows: false,
20 | showDots: false,
21 | showNavigationOnHover: false,
22 | isDraggable: true,
23 | },
24 | } satisfies Meta;
25 |
26 | export default meta;
27 | type Story = StoryObj;
28 |
29 | const images = [
30 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/La_Tour_Eiffel_vue_de_la_Tour_Saint-Jacques%2C_Paris_ao%C3%BBt_2014_%282%29.jpg/1200px-La_Tour_Eiffel_vue_de_la_Tour_Saint-Jacques%2C_Paris_ao%C3%BBt_2014_%282%29.jpg',
31 | 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/02/57/44/0c/filename-img-1097-jpg.jpg?w=700&h=-1&s=1',
32 | 'https://imageio.forbes.com/specials-images/imageserve/646b6b45d9b20ac15900fd8a/0x0.jpg?format=jpg&width=1200',
33 | ];
34 |
35 | export const Default: Story = {
36 | render: ({ ...args }) => {
37 | return ;
38 | },
39 | };
40 |
41 | export const WithArrowButtons: Story = {
42 | render: ({ ...args }) => {
43 | return ;
44 | },
45 | args: {
46 | showArrows: true,
47 | },
48 | };
49 |
50 | export const WithDots: Story = {
51 | render: ({ ...args }) => {
52 | return ;
53 | },
54 | args: {
55 | showDots: true,
56 | },
57 | };
58 |
59 | export const ShowNavigationOnHover: Story = {
60 | render: ({ ...args }) => {
61 | return ;
62 | },
63 | args: {
64 | showArrows: true,
65 | showDots: true,
66 | showNavigationOnHover: true,
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/src/stories/ImageUploadInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import ImageUploadInput from '@components/ImageUploadInput/ImageUploadInput';
4 |
5 | const meta = {
6 | title: 'ImageUploadInput',
7 | component: ImageUploadInput,
8 | argTypes: {
9 | label: {
10 | control: { type: 'text' },
11 | },
12 | supportingText: {
13 | control: { type: 'text' },
14 | },
15 | maxUploadCount: {
16 | control: { type: 'number' },
17 | },
18 | },
19 | args: {
20 | imageAltText: '이미지',
21 | imageUrls: null,
22 | maxUploadCount: 2,
23 | },
24 | } satisfies Meta;
25 |
26 | export default meta;
27 | type Story = StoryObj;
28 |
29 | export const Default: Story = {};
30 |
31 | export const WithUploadedImages: Story = {
32 | args: {
33 | imageAltText: '이미지',
34 | imageUrls: [
35 | 'https://a.cdn-hotels.com/gdcs/production163/d1616/24e46678-07e1-4f27-93d3-9eb979c2ae5e.jpg',
36 | 'https://cdn.sortiraparis.com/images/80/83517/529854-visuel-paris-marais.jpg',
37 | ],
38 | maxUploadCount: 3,
39 | },
40 | };
41 |
42 | export const MaximumImagesUploaded: Story = {
43 | args: {
44 | imageAltText: '이미지',
45 | imageUrls: [
46 | 'https://a.cdn-hotels.com/gdcs/production163/d1616/24e46678-07e1-4f27-93d3-9eb979c2ae5e.jpg',
47 | 'https://cdn.sortiraparis.com/images/80/83517/529854-visuel-paris-marais.jpg',
48 | ],
49 | maxUploadCount: 2,
50 | },
51 | };
52 |
--------------------------------------------------------------------------------
/src/stories/Label.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Label from '@components/Label/Label';
4 |
5 | const meta = {
6 | title: 'Label',
7 | component: Label,
8 | argTypes: {
9 | children: {
10 | control: { type: 'text' },
11 | },
12 | required: {
13 | control: { type: 'boolean' },
14 | },
15 | },
16 | args: {
17 | children: 'Label',
18 | required: false,
19 | },
20 | } satisfies Meta;
21 |
22 | export default meta;
23 | type Story = StoryObj;
24 |
25 | export const Default: Story = {};
26 |
--------------------------------------------------------------------------------
/src/stories/Menu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Button from '@components/Button/Button';
4 | import Menu from '@components/Menu/Menu';
5 | import MenuItem from '@components/MenuItem/MenuItem';
6 | import MenuList from '@components/MenuList/MenuList';
7 |
8 | import { useOverlay } from '..';
9 |
10 | const meta = {
11 | title: 'Menu',
12 | component: Menu,
13 | args: {},
14 | } satisfies Meta;
15 |
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {
20 | render: () => {
21 | const { isOpen, toggle, close } = useOverlay();
22 |
23 | return (
24 |
33 | );
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/src/stories/RadioButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import RadioButton from '@components/RadioButton/RadioButton';
4 |
5 | const meta = {
6 | title: 'RadioButton',
7 | component: RadioButton,
8 | args: {
9 | options: ['option1', 'option2'],
10 | name: 'sample',
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Playground: Story = {
18 | args: {
19 | label: 'Label',
20 | supportingText: 'Supporting text',
21 | },
22 | };
23 |
24 | export const Default: Story = {};
25 |
26 | export const WithLabel: Story = {
27 | args: {
28 | label: 'Label',
29 | },
30 | name: 'Input with Label',
31 | };
32 |
33 | export const WithSupportingText: Story = {
34 | args: {
35 | supportingText: 'Supporting Text',
36 | },
37 | name: 'Input with Supporting Text',
38 | };
39 |
40 | export const WithLabelAndSupportingText: Story = {
41 | args: {
42 | label: 'Label',
43 | supportingText: 'Supporting Text',
44 | required: true,
45 | },
46 | name: 'Input with Label and Supporting Text',
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/SVGCarousel.stories.tsx:
--------------------------------------------------------------------------------
1 | import icon1 from '@assets/svg/add-icon.svg';
2 | import icon2 from '@assets/svg/checked-icon.svg';
3 | import icon3 from '@assets/svg/empty-star.svg';
4 | import type { Meta, StoryObj } from '@storybook/react';
5 |
6 | import SVGCarousel from '@components/SVGCarousel/SVGCarousel';
7 |
8 | const meta = {
9 | title: 'SVGCarousel',
10 | component: SVGCarousel,
11 | argTypes: {
12 | width: { control: 'number' },
13 | height: { control: 'number' },
14 | showArrows: { control: 'boolean' },
15 | showDots: { control: 'boolean' },
16 | showNavigationOnHover: { control: 'boolean' },
17 | images: { control: false },
18 | },
19 | args: {
20 | width: 250,
21 | height: 167,
22 | showArrows: false,
23 | showDots: false,
24 | showNavigationOnHover: false,
25 | },
26 | } satisfies Meta;
27 |
28 | export default meta;
29 | type Story = StoryObj;
30 |
31 | const images = [icon1, icon2, icon3];
32 |
33 | export const Default: Story = {
34 | render: ({ ...args }) => {
35 | return ;
36 | },
37 | };
38 |
39 | export const WithArrowButtons: Story = {
40 | render: ({ ...args }) => {
41 | return ;
42 | },
43 | args: {
44 | showArrows: true,
45 | },
46 | };
47 |
48 | export const WithDots: Story = {
49 | render: ({ ...args }) => {
50 | return ;
51 | },
52 | args: {
53 | showDots: true,
54 | },
55 | };
56 |
57 | export const ShowNavigationOnHover: Story = {
58 | render: ({ ...args }) => {
59 | return ;
60 | },
61 | args: {
62 | showArrows: true,
63 | showDots: true,
64 | showNavigationOnHover: true,
65 | },
66 | };
67 |
--------------------------------------------------------------------------------
/src/stories/SVGCarouselModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import icon1 from '@assets/svg/add-icon.svg';
2 | import icon2 from '@assets/svg/checked-icon.svg';
3 | import icon3 from '@assets/svg/empty-star.svg';
4 | import type { Meta, StoryObj } from '@storybook/react';
5 |
6 | import SVGCarouselModal from '@components/SVGCarouselModal/SVGCarouselModal';
7 |
8 | const meta = {
9 | title: 'SVGCarouselModal',
10 | component: SVGCarouselModal,
11 | argTypes: {
12 | isOpen: { control: 'boolean' },
13 | closeModal: { control: false },
14 | carouselWidth: { control: 'number' },
15 | carouselHeight: { control: 'number' },
16 | showArrows: { control: 'boolean' },
17 | showDots: { control: 'boolean' },
18 | showNavigationOnHover: { control: 'boolean' },
19 | carouselImages: { control: false },
20 | },
21 | args: {
22 | isOpen: true,
23 | closeModal: () => {},
24 | carouselWidth: 450,
25 | carouselHeight: 450,
26 | showArrows: false,
27 | showDots: false,
28 | showNavigationOnHover: false,
29 | },
30 | } satisfies Meta;
31 |
32 | export default meta;
33 | type Story = StoryObj;
34 |
35 | const images = [icon1, icon2, icon3];
36 |
37 | export const Default: Story = {
38 | render: ({ ...args }) => {
39 | return ;
40 | },
41 | args: {
42 | showArrows: true,
43 | showDots: true,
44 | modalWidth: 300,
45 | modalHeight: 300,
46 | buttonGap: 48,
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/stories/Select.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Select from '@components/Select/Select';
5 |
6 | const meta = {
7 | title: 'Select',
8 | component: Select,
9 | argTypes: {
10 | label: {
11 | control: { type: 'text' },
12 | description: 'Select의 시이즈',
13 | },
14 | size: {
15 | control: { type: 'radio' },
16 | options: ['small', 'medium', 'large'],
17 | description: 'Select의 시이즈',
18 | },
19 | isError: {
20 | control: { type: 'boolean' },
21 | description: 'Select 인풋의 에러 여부',
22 | },
23 | supportingText: {
24 | control: { type: 'text' },
25 | description: 'Select의 부가 정보 텍스트',
26 | },
27 | required: {
28 | control: { type: 'boolean' },
29 | description: 'Select의 필수 입력 여부',
30 | },
31 | children: {
32 | description: 'Select에서 선택할 수 있는 JSX option 요소들',
33 | },
34 | },
35 | args: {
36 | size: 'medium',
37 | isError: false,
38 | required: false,
39 | children: [
40 | ,
41 | ,
42 | ,
43 | ],
44 | },
45 | } satisfies Meta;
46 |
47 | export default meta;
48 | type Story = StoryObj;
49 |
50 | export const Playground: Story = {};
51 |
52 | export const Sizes: Story = {
53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
54 | render: ({ isError, placeholder, children }) => {
55 | return (
56 |
57 | -
58 |
Small
59 |
62 |
63 | -
64 |
Medium
65 |
66 |
67 | -
68 |
Large
69 |
72 |
73 |
74 | );
75 | },
76 | argTypes: {
77 | size: {
78 | control: false,
79 | },
80 | },
81 | };
82 |
83 | export const WithLabel: Story = {
84 | args: {
85 | label: 'Label',
86 | },
87 | name: 'Select with Label',
88 | };
89 |
90 | export const WithSupportingText: Story = {
91 | args: {
92 | supportingText: 'Supporting Text',
93 | },
94 | name: 'Select with Supporting Text',
95 | };
96 |
97 | export const WithLabelAndSupportingText: Story = {
98 | args: {
99 | label: 'Label',
100 | supportingText: 'Supporting Text',
101 | required: true,
102 | },
103 | name: 'Select with Label and Supporting Text',
104 | };
105 |
--------------------------------------------------------------------------------
/src/stories/Skeleton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Skeleton from '@components/Skeleton/Skeleton';
5 |
6 | const meta = {
7 | title: 'Skeleton',
8 | component: Skeleton,
9 | args: {
10 | width: '500px',
11 | height: '24px',
12 | },
13 | } satisfies Meta;
14 |
15 | export default meta;
16 | type Story = StoryObj;
17 |
18 | export const Playground: Story = {};
19 |
20 | export const Default: Story = {
21 | argTypes: {
22 | width: { control: false },
23 | height: { control: false },
24 | variant: { control: false },
25 | },
26 | };
27 |
28 | export const Image: Story = {
29 | args: {
30 | width: '450px',
31 | height: '300px',
32 | },
33 | };
34 |
35 | export const Paragraph: Story = {
36 | args: {
37 | width: '400px',
38 | height: '100px',
39 | },
40 | };
41 |
42 | export const Circle: Story = {
43 | args: {
44 | width: '200px',
45 | variant: 'circle',
46 | },
47 | argTypes: {
48 | variant: { control: false },
49 | },
50 | };
51 |
52 | export const Combination: Story = {
53 | render: () => {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | },
63 | argTypes: {
64 | variant: { control: false },
65 | width: { control: false },
66 | height: { control: false },
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/src/stories/Spinner.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Spinner from '@components/Spinner/Spinner';
4 |
5 | const meta = {
6 | title: 'Spinner',
7 | component: Spinner,
8 | args: {
9 | timing: 1,
10 | size: 50,
11 | width: 5,
12 | disabled: false,
13 | },
14 | } satisfies Meta;
15 |
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {};
20 |
21 | export const Disabled: Story = {
22 | args: {
23 | disabled: true,
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/stories/StarRatingInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { useStarRatingInput } from '@hooks/useStarRatingInput';
4 |
5 | import StarRatingInput from '@components/StarRatingInput/StarRatingInput';
6 |
7 | const meta = {
8 | title: 'StarRatingInput',
9 | component: StarRatingInput,
10 | argTypes: {
11 | rate: {
12 | control: false,
13 | },
14 | size: {
15 | control: { type: 'number' },
16 | description: 'size',
17 | },
18 | gap: {
19 | control: { type: 'number' },
20 | description: 'gap',
21 | },
22 | isMobile: {
23 | control: { type: 'boolean' },
24 | description: 'isMobile',
25 | },
26 | },
27 | args: {
28 | isMobile: true,
29 | label: '별점',
30 | supportingText: '별점을 입력해 주세요',
31 | rate: 1,
32 | size: 24,
33 | gap: 2,
34 | },
35 | } satisfies Meta;
36 |
37 | export default meta;
38 | type Story = StoryObj;
39 |
40 | export const Playground: Story = {
41 | render: ({ size, gap, ...args }) => {
42 | const { starRate, handleStarClick, handleStarHover, handleStarHoverOut } =
43 | useStarRatingInput(2.5);
44 |
45 | return (
46 |
55 | );
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/src/stories/SupportingText.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import SupportingText from '@components/SupportingText/SupportingText';
4 |
5 | const meta = {
6 | title: 'SupportingText',
7 | component: SupportingText,
8 | argTypes: {
9 | children: {
10 | control: { type: 'text' },
11 | },
12 | isError: {
13 | control: { type: 'boolean' },
14 | },
15 | },
16 | args: {
17 | children: 'Supporting text',
18 | isError: false,
19 | },
20 | } satisfies Meta;
21 |
22 | export default meta;
23 | type Story = StoryObj;
24 |
25 | export const Playground: Story = {};
26 |
27 | export const Default: Story = {
28 | argTypes: {
29 | isError: {
30 | control: false,
31 | },
32 | },
33 | };
34 |
35 | export const Error: Story = {
36 | args: {
37 | isError: true,
38 | },
39 | argTypes: {
40 | isError: {
41 | control: false,
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/src/stories/SwitchToggle.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 | import { ChangeEvent } from 'react';
4 |
5 | import SwitchToggle from '@components/SwitchToggle/SwitchToggle';
6 |
7 | const meta = {
8 | title: 'SwitchToggle',
9 | component: SwitchToggle,
10 | args: {
11 | onChange: (e: ChangeEvent) => {},
12 | checkedState: false,
13 | },
14 | } satisfies Meta;
15 |
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const Playground: Story = {};
20 |
--------------------------------------------------------------------------------
/src/stories/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import type { TextProps } from '@components/Text/Text';
5 | import Text from '@components/Text/Text';
6 |
7 | const meta = {
8 | title: 'Text',
9 | component: Text,
10 | argTypes: {
11 | size: {
12 | control: { type: 'radio' },
13 | options: ['xSmall', 'small', 'medium', 'large'],
14 | },
15 | children: {
16 | control: { type: 'text' },
17 | },
18 | },
19 | args: {
20 | size: 'medium',
21 | children: 'Text',
22 | },
23 | } satisfies Meta;
24 |
25 | export default meta;
26 | type Story = StoryObj;
27 |
28 | const createTextStory = (size: TextProps['size']) => ({
29 | args: {
30 | size,
31 | },
32 | argTypes: {
33 | size: {
34 | control: false,
35 | },
36 | },
37 | });
38 |
39 | export const Playground: Story = {};
40 |
41 | export const Sizes: Story = {
42 | render: ({ children }) => {
43 | return (
44 |
45 | -
46 |
X Small
47 | {children}
48 |
49 | -
50 |
Small
51 | {children}
52 |
53 | -
54 |
Medium
55 | {children}
56 |
57 | -
58 |
Large
59 | {children}
60 |
61 |
62 | );
63 | },
64 | argTypes: {
65 | size: {
66 | control: false,
67 | },
68 | },
69 | };
70 |
71 | export const XSmall: Story = createTextStory('xSmall');
72 |
73 | export const Small: Story = createTextStory('small');
74 |
75 | export const Medium: Story = createTextStory('medium');
76 |
77 | export const Large: Story = createTextStory('large');
78 |
--------------------------------------------------------------------------------
/src/stories/Textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import Textarea from '@components/Textarea/Textarea';
5 |
6 | const meta = {
7 | title: 'Textarea',
8 | component: Textarea,
9 | argTypes: {
10 | label: {
11 | control: { type: 'text' },
12 | },
13 | size: {
14 | control: { type: 'radio' },
15 | options: ['small', 'medium', 'large'],
16 | },
17 | isError: {
18 | control: { type: 'boolean' },
19 | },
20 | supportingText: {
21 | control: { type: 'text' },
22 | },
23 | required: {
24 | control: { type: 'boolean' },
25 | },
26 | },
27 | args: {
28 | size: 'medium',
29 | placeholder: 'placeholder',
30 | isError: false,
31 | required: false,
32 | },
33 | } satisfies Meta;
34 |
35 | export default meta;
36 | type Story = StoryObj;
37 |
38 | export const Playground: Story = {};
39 |
40 | export const Sizes: Story = {
41 | render: ({ isError, placeholder }) => {
42 | return (
43 |
44 | -
45 |
Small
46 |
47 |
48 | -
49 |
Medium
50 |
51 |
52 | -
53 |
Large
54 |
55 |
56 |
57 | );
58 | },
59 | argTypes: {
60 | size: {
61 | control: false,
62 | },
63 | },
64 | };
65 |
66 | export const WithLabel: Story = {
67 | args: {
68 | label: 'Label',
69 | },
70 | name: 'Textarea with Label',
71 | };
72 |
73 | export const WithSupportingText: Story = {
74 | args: {
75 | supportingText: 'Supporting Text',
76 | },
77 | name: 'Textarea with Supporting Text',
78 | };
79 |
80 | export const WithLabelAndSupportingText: Story = {
81 | args: {
82 | label: 'Label',
83 | supportingText: 'Supporting Text',
84 | required: true,
85 | },
86 | name: 'Textarea with Label and Supporting Text',
87 | };
88 |
--------------------------------------------------------------------------------
/src/stories/ToggleGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import { containerStyle, informationStyle } from '@stories/styles';
2 | import type { Meta, StoryObj } from '@storybook/react';
3 |
4 | import { useSelect } from '@hooks/useSelect';
5 |
6 | import Toggle from '@components/Toggle/Toggle';
7 | import ToggleGroup from '@components/ToggleGroup/ToggleGroup';
8 |
9 | const meta = {
10 | title: 'ToggleGroup',
11 | component: ToggleGroup,
12 | decorators: [
13 | (Story) => (
14 |
17 | ),
18 | ],
19 | } satisfies Meta;
20 |
21 | export default meta;
22 | type Story = StoryObj;
23 |
24 | export const Variants: Story = {
25 | render: () => {
26 | const { selected, handleSelectClick } = useSelect('toggle1');
27 |
28 | return (
29 |
30 | Toggle Group and Toggle
31 |
32 |
38 |
44 |
50 |
51 |
52 | );
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/src/stories/styles.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | import { Theme } from '../styles/Theme';
4 |
5 | const containerStyle = css({
6 | display: 'flex',
7 | flexDirection: 'column',
8 | gap: '24px',
9 | alignItems: 'flex-start',
10 |
11 | width: '300px',
12 | });
13 |
14 | const informationStyle = css({
15 | display: 'flex',
16 | flexDirection: 'column',
17 | gap: '12px',
18 |
19 | '& > h6': {
20 | color: Theme.color.gray500,
21 | fontSize: '12px',
22 | fontWeight: 400,
23 | textTransform: 'uppercase',
24 | },
25 | });
26 |
27 | const containerWrapperStyle = css({
28 | position: 'absolute',
29 | top: '0',
30 | left: '0',
31 | display: 'flex',
32 | flexDirection: 'column',
33 | alignItems: 'center',
34 |
35 | width: '100%',
36 | height: '100%',
37 | padding: '20px',
38 | });
39 |
40 | export { containerStyle, informationStyle, containerWrapperStyle };
41 |
--------------------------------------------------------------------------------
/src/styles/GlobalStyle.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | import { Theme } from '@styles/Theme';
4 |
5 | export const GlobalStyle = css({
6 | '*': {
7 | padding: 0,
8 | margin: 0,
9 | boxSizing: 'border-box',
10 | },
11 |
12 | 'ul, ol, li': {
13 | listStyle: 'none',
14 | },
15 |
16 | 'html, body': {
17 | fontFamily: `system-ui, -apple-system, BlinkMacSystemFont, 'Open Sans', 'Helvetica Neue'`,
18 | fontSize: '16px',
19 | color: Theme.color.gray800,
20 | },
21 |
22 | a: {
23 | textDecoration: 'none',
24 | color: Theme.color.blue700,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/styles/Theme.ts:
--------------------------------------------------------------------------------
1 | const color = {
2 | /** heading text */
3 | black: 'black',
4 | /** default text */
5 | gray800: '#282828',
6 | gray700: '#5e5e5e',
7 | /** light text */
8 | gray600: '#727272',
9 | gray500: '#a6a6a6',
10 | gray400: '#bbbbbb',
11 | gray300: '#dddddd',
12 | /** border */
13 | gray200: '#e8e8e8',
14 | /** background */
15 | gray100: '#f3f3f3',
16 | /** background */
17 | white: 'white',
18 |
19 | blue800: '#004765',
20 | blue700: '#006f9f',
21 | blue600: '#009ee2',
22 | /** primary color */
23 | blue500: '#00b2ff',
24 | blue400: '#80d9ff',
25 | blue300: '#bbebff',
26 | blue200: '#d8f3ff',
27 | blue100: '#eaf9ff',
28 |
29 | /** dark red */
30 | red300: '#c50000',
31 | /** red */
32 | red200: '#ea0000',
33 | /** light red */
34 | red100: '#fff2f2',
35 |
36 | green: '#2FC56E',
37 | } as const;
38 |
39 | const text = {
40 | large: {
41 | fontSize: '18px',
42 | lineHeight: '28px',
43 | },
44 | /** default text font setting */
45 | medium: {
46 | fontSize: '16px',
47 | lineHeight: '24px',
48 | },
49 | small: {
50 | fontSize: '14px',
51 | lineHeight: '20px',
52 | },
53 | xSmall: {
54 | fontSize: '12px',
55 | lineHeight: '20px',
56 | },
57 | } as const;
58 |
59 | const heading = {
60 | xxLarge: {
61 | fontSize: '40px',
62 | lineHeight: '52px',
63 | },
64 | xLarge: {
65 | fontSize: '36px',
66 | lineHeight: '44px',
67 | },
68 | large: {
69 | fontSize: '32px',
70 | lineHeight: '40px',
71 | },
72 | /** default heading font setting */
73 | medium: {
74 | fontSize: '28px',
75 | lineHeight: '36px',
76 | },
77 | small: {
78 | fontSize: '24px',
79 | lineHeight: '32px',
80 | },
81 | xSmall: {
82 | fontSize: '20px',
83 | lineHeight: '28px',
84 | },
85 | } as const;
86 |
87 | const spacer = {
88 | spacing0: '0',
89 | spacing1: '4px',
90 | spacing2: '8px',
91 | spacing3: '16px',
92 | spacing4: '24px',
93 | spacing5: '32px',
94 | spacing6: '48px',
95 | spacing7: '64px',
96 | spacing8: '96px',
97 | spacing9: '128px',
98 | } as const;
99 |
100 | const borderRadius = {
101 | small: '4px',
102 | /** default border radius */
103 | medium: '8px',
104 | large: '16px',
105 | } as const;
106 |
107 | const boxShadow = {
108 | shadow1: '0px 0px 0px 1px rgba(0, 0, 0, 0.05)',
109 | shadow2: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)',
110 | shadow3: '0px 1px 2px 0px rgba(0, 0, 0, 0.06), 0px 1px 3px 0px rgba(0, 0, 0, 0.10)',
111 | shadow4: '0px 2px 4px -1px rgba(0, 0, 0, 0.06), 0px 4px 6px -1px rgba(0, 0, 0, 0.10)',
112 | shadow5: '1px 2px 4px 0px rgba(0, 0, 0, 0.15)',
113 | shadow6: '0px 4px 6px -2px rgba(0, 0, 0, 0.05), 0px 10px 15px -3px rgba(0, 0, 0, 0.10)',
114 | shadow7: '0px 10px 10px -5px rgba(0, 0, 0, 0.04), 0px 20px 25px -5px rgba(0, 0, 0, 0.10)',
115 | shadow8: '0px 0px 5px 0px rgba(0, 0, 0, 0.15)',
116 | shadow9: '0px 0px 10px 0px rgba(0, 0, 0, 0.20)',
117 | shadow10: '0px 2px 4px 0px rgba(0, 0, 0, 0.06) inset',
118 | } as const;
119 |
120 | const zIndex = {
121 | overlayPeak: 4,
122 | overlayTop: 3,
123 | overlayMiddle: 2,
124 | overlayBottom: 1,
125 | } as const;
126 |
127 | export const Theme = {
128 | color,
129 | text,
130 | heading,
131 | spacer,
132 | borderRadius,
133 | boxShadow,
134 | zIndex,
135 | };
136 |
--------------------------------------------------------------------------------
/src/styles/animation/index.ts:
--------------------------------------------------------------------------------
1 | import { keyframes } from '@emotion/react';
2 |
3 | export const spinnerRotation = keyframes`
4 | 0% {
5 | transform: rotate(0deg);
6 | }
7 | 100% {
8 | transform: rotate(360deg);
9 | }
10 | `;
11 |
12 | export const fadeIn = keyframes`
13 | from {
14 | opacity: 0;
15 | }
16 | to {
17 | opacity: 1;
18 | }
19 | `;
20 |
21 | export const fadeOut = keyframes`
22 | from {
23 | opacity: 1;
24 | }
25 | to {
26 | opacity: 0;
27 | }
28 | `;
29 |
30 | export const moveUp = keyframes`
31 | from {
32 | transform: translateY(50%);
33 | }
34 | to {
35 | transform: translateY(0%);
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/src/styles/style.d.ts:
--------------------------------------------------------------------------------
1 | import '@emotion/react';
2 |
3 | interface TextStyle {
4 | [key: string]: {
5 | fontSize: string;
6 | lineHeight: string;
7 | };
8 | }
9 |
10 | declare module '@emotion/react' {
11 | export interface Theme {
12 | color: { [key: string]: string };
13 | text: TextStyle;
14 | heading: TextStyle;
15 | spacer: { [key: string]: string };
16 | borderRadius: { [key: string]: string };
17 | boxShadow: { [key: string]: string };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/types/date.ts:
--------------------------------------------------------------------------------
1 | export interface YearMonth {
2 | /** 년도 문자열 */
3 | year: string;
4 | /** 월 문자열 - '06' 포맷 */
5 | month: string;
6 | /** 월의 시작일 Date */
7 | startDate: Date;
8 | /** 월의 첫 번째 요일 */
9 | firstDay: number;
10 | /** 월의 마지막 번째 날 */
11 | lastDate: number;
12 | }
13 |
14 | export interface DayInfo {
15 | /** 월의 Day 박스의 인덱스 */
16 | index: number;
17 | /** 현재 년월 정보 */
18 | yearMonthData: YearMonth;
19 | /** 현재 Date */
20 | currentDate: Date;
21 | /** 현재 선택된 날짜 범위 */
22 | dateRange?: SelectedDateRange;
23 | /** 최대로 선택할 수 있는 날짜 범위 */
24 | maxDateRange?: number;
25 | /** 오늘 이후 날짜를 막을 것인지에 대한 여부 */
26 | isFutureDaysRestricted?: boolean;
27 | /** 특정 범위를 벗어나는 날짜에 대해서 선택 불가능할지에 대한 여부 */
28 | hasRangeRestriction?: boolean;
29 | /** 현재 선택된 날짜 */
30 | selectedDate?: number;
31 | }
32 |
33 | export interface DateRangePickerCalendar {
34 | prevYearMonth: YearMonth;
35 | currentYearMonth: YearMonth;
36 | }
37 |
38 | export interface SelectedDateRange {
39 | startDate: string | null;
40 | endDate: string | null;
41 | }
42 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type Size = 'xSmall' | 'small' | 'medium' | 'large' | 'xLarge' | 'xxLarge';
2 |
--------------------------------------------------------------------------------
/src/utils/number.ts:
--------------------------------------------------------------------------------
1 | export const limitToRange = (value: number, minValue: number, maxValue: number) => {
2 | if (value < minValue) return minValue;
3 |
4 | if (value > maxValue) return maxValue;
5 |
6 | return value;
7 | };
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist",
4 | "target": "ES5",
5 | "skipLibCheck": true,
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "declaration": true,
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
13 | "jsx": "react-jsx",
14 | "allowJs": true,
15 | "baseUrl": ".",
16 | "paths": {
17 | "@/*": ["src/*"],
18 | "@components/*": ["src/components/*"],
19 | "@type/*": ["src/types/*"],
20 | "@hooks/*": ["src/hooks/*"],
21 | "@styles/*": ["src/styles/*"],
22 | "@constants/*": ["src/constants/*"],
23 | "@assets/*": ["src/assets/*"],
24 | "@stories/*": ["src/stories/*"],
25 | "@utils/*": ["src/utils/*"]
26 | },
27 | "jsxImportSource": "@emotion/react",
28 | "allowSyntheticDefaultImports": true
29 | },
30 | "include": ["src"],
31 | "exclude": ["./node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const prod = (process.env.NODE_ENV = 'production');
5 |
6 | module.exports = {
7 | mode: prod ? 'production' : 'development',
8 | entry: './src/index.tsx',
9 | resolve: {
10 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
11 | },
12 |
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(js|jsx|ts|tsx)$/,
17 | exclude: /node_modules/,
18 | use: ['ts-loader'],
19 | },
20 | {
21 | test: /\.svg$/i,
22 | issuer: /\.[jt]sx?$/,
23 | use: ['@svgr/webpack'],
24 | },
25 | {
26 | test: /\.svg$/i,
27 | issuer: /\.style.js|style.ts$/,
28 | use: ['url-loader'],
29 | },
30 | ],
31 | },
32 | output: {
33 | path: path.join(__dirname, '/dist'),
34 | filename: 'bundle.js',
35 | },
36 |
37 | devServer: {
38 | historyApiFallback: true,
39 | port: 3000,
40 | hot: true,
41 | static: path.resolve(__dirname, 'dist'),
42 | },
43 |
44 | resolve: {
45 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
46 | alias: {
47 | '@': path.resolve(__dirname, './src'),
48 | '@components': path.resolve(__dirname, './src/components'),
49 | '@type': path.resolve(__dirname, './src/types'),
50 | '@hooks': path.resolve(__dirname, './src/hooks'),
51 | '@styles': path.resolve(__dirname, './src/styles'),
52 | '@constants': path.resolve(__dirname, './src/constants'),
53 | '@assets': path.resolve(__dirname, './src/assets'),
54 | '@stories': path.resolve(__dirname, './src/stories'),
55 | '@utils': path.resolve(__dirname, './src/utils'),
56 | },
57 | },
58 |
59 | plugins: [
60 | new webpack.ProvidePlugin({
61 | React: 'react',
62 | }),
63 | new HtmlWebpackPlugin({
64 | template: './public/index.html',
65 | }),
66 | new webpack.HotModuleReplacementPlugin(),
67 | ],
68 | };
69 |
--------------------------------------------------------------------------------