diff --git a/.github/pull_request_template.yml b/.github/PULL_REQUEST_TEMPLATE/solo-template.yml similarity index 100% rename from .github/pull_request_template.yml rename to .github/PULL_REQUEST_TEMPLATE/solo-template.yml diff --git a/src/assets/check-mark.svg b/src/assets/check-mark.svg new file mode 100644 index 0000000..542b426 --- /dev/null +++ b/src/assets/check-mark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/componets/Button/Button.scss b/src/componets/Button/Button.scss index c80e690..5107506 100644 --- a/src/componets/Button/Button.scss +++ b/src/componets/Button/Button.scss @@ -1,17 +1,16 @@ -@use '../../styles/index' as v; +@use '../../styles/index' as s; +@use 'sass:map'; -.xp-btn { +.btn { background: #d4d4d4; - color: #5a5a5a; + @include s.text-style-extended(lg, 400, gray-900); border: none; - font-family: 'Tahoma', 'Verdana', sans-serif; - font-size: 16px; padding: 6px 18px; cursor: pointer; box-shadow: - inset 1px 1px 0 0 #fff, - 1px 1px 0 0 #868686, - 2px 2px 0 0 #222; + inset 1px 1px 0 0 s.color(white), + 1px 1px 0 0 s.color(gray-600), + 2px 2px 0 0 s.color(gray-900); transition: background-color 0.12s, color 0.12s, @@ -19,19 +18,19 @@ &:hover, &:focus { - background: #e5e5e5; - color: #222; + background: s.color(gray-300); + color: s.color(gray-900); box-shadow: - inset 1px 1px 0 0 #fff, - 2px 2px 0 0 #222; + inset 1px 1px 0 0 s.color(white), + 2px 2px 0 0 s.color(gray-900); } &:active { background: #bbbbbb; - color: #222; + color: s.color(gray-900); box-shadow: - inset 1px 1px 0 0 #fff, + inset 1px 1px 0 0 s.color(white), 1px 1px 0 0 #868686, - 0px 0px 0 0 #222; + 0px 0px 0 0 s.color(gray-900); } @media (max-width: 640px) { @@ -41,30 +40,30 @@ } // 빨강 버튼 (secondary) -.xp-btn.secondary { - background: #d43c3c; - color: #fff; +.btn.secondary { + background: s.color(red_btn); + color: s.color(white); border: none; box-shadow: - inset 1px 1px 0 0 #fff, - 1px 1px 0 0 #b92a2a, - 2px 2px 0 0 #7a1818; + inset 1px 1px 0 0 s.color(white), + 1px 1px 0 0 s.color(red_btn_hover), + 2px 2px 0 0 s.color(red_btn_shadow); &:hover, &:focus { - background: #b92a2a; - color: #fff; + background: s.color(red_btn_hover); + color: s.color(white); box-shadow: - inset 1px 1px 0 0 #fff, - 2px 2px 0 0 #7a1818; + inset 1px 1px 0 0 s.color(white), + 2px 2px 0 0 s.color(red_btn_shadow); } &:active { - background: #a12222; - color: #fff; + background: s.color(red_btn_active); + color: s.color(white); box-shadow: - inset 1px 1px 0 0 #fff, - 1px 1px 0 0 #b92a2a, - 0px 0px 0 0 #7a1818; + inset 1px 1px 0 0 s.color(white), + 1px 1px 0 0 s.color(red_btn_hover), + 0px 0px 0 0 s.color(red_btn_shadow); } @media (max-width: 640px) { diff --git a/src/componets/Button/Button.stories.tsx b/src/componets/Button/Button.stories.tsx index 1179b75..1c30925 100644 --- a/src/componets/Button/Button.stories.tsx +++ b/src/componets/Button/Button.stories.tsx @@ -12,8 +12,16 @@ type Story = StoryObj; export const Primary: Story = { args: { - children: '클릭', + children: 'Button', variant: 'default', color: 'rgba(255, 0, 0, 1)' } }; + +export const Secondary: Story = { + args: { + children: 'Button', + variant: 'secondary', + color: 'rgba(255, 0, 0, 1)' + } +}; diff --git a/src/componets/Button/Button.test.tsx b/src/componets/Button/Button.test.tsx index f979c16..73371da 100644 --- a/src/componets/Button/Button.test.tsx +++ b/src/componets/Button/Button.test.tsx @@ -1,12 +1,13 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { test, expect, vi } from "vitest"; -import Button from "./Button"; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { test, expect, vi } from 'vitest'; -test("renders 버튼과 클릭", async () => { +import Button from './Button'; + +test('renders 버튼과 클릭', async () => { const onClick = vi.fn(); render(); - const btn = screen.getByRole("button", { name: /클릭/i }); + const btn = screen.getByRole('button', { name: /클릭/i }); await userEvent.click(btn); expect(onClick).toHaveBeenCalled(); }); diff --git a/src/componets/Button/Button.tsx b/src/componets/Button/Button.tsx index 3beca5d..5266b30 100644 --- a/src/componets/Button/Button.tsx +++ b/src/componets/Button/Button.tsx @@ -1,17 +1,18 @@ import React from 'react'; import './Button.scss'; -export type ButtonProps = React.ButtonHTMLAttributes & { +export interface ButtonProps + extends React.ButtonHTMLAttributes { variant?: 'default' | 'secondary'; -}; +} const Button: React.FC = ({ children, variant = 'default', ...rest }) => { - // xp-btn, secondary 클래스 동적 조합 - const classNames = ['xp-btn', variant === 'secondary' ? 'secondary' : ''] + // btn, secondary 클래스 동적 조합 + const classNames = ['btn', variant !== 'default' && variant] .filter(Boolean) .join(' '); diff --git a/src/componets/Checkbox/Checkbox.scss b/src/componets/Checkbox/Checkbox.scss new file mode 100644 index 0000000..bece911 --- /dev/null +++ b/src/componets/Checkbox/Checkbox.scss @@ -0,0 +1,55 @@ +@use '../../styles/index' as s; +@use 'sass:map'; + +.checkbox { + display: flex; + align-items: center; + gap: 8px; + position: relative; + width: fit-content; + height: fit-content; +} + +.checkbox_png { + position: absolute; + left: 3px; + top: -5px; + width: 20px; + height: 20px; + background: url('../../assets/check-mark.svg') center center no-repeat; + background-size: contain; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s; + z-index: 3; + @media (max-width: 640px) { + width: 16px; + height: 16px; + left: 4px; + top: -2px; + } +} + +.checkbox_input { + appearance: none; + width: 18px; + height: 18px; + border-radius: 0; + border: 2px solid s.color(gray-400); + background: s.color(white); + cursor: pointer; + transition: + background-color 0.2s, + border-color 0.2s; + + &:checked { + ~ .checkbox_png { + opacity: 1; + } + } + + @media (max-width: 640px) { + width: 16px; + height: 16px; + } +} diff --git a/src/componets/Checkbox/Checkbox.stories.tsx b/src/componets/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000..2a3cf79 --- /dev/null +++ b/src/componets/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import Checkbox from './Checkbox'; + +const meta: Meta = { + title: 'Components/Checkbox', + component: Checkbox, + argTypes: { onClick: { action: 'changed' } } +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + label: 'Checkbox Default' + } +}; + +export const Secondary: Story = { + args: { + label: 'Checkbox Checked', + checked: true + } +}; diff --git a/src/componets/Checkbox/Checkbox.test.tsx b/src/componets/Checkbox/Checkbox.test.tsx new file mode 100644 index 0000000..bc8640d --- /dev/null +++ b/src/componets/Checkbox/Checkbox.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { test, expect, vi } from 'vitest'; + +import Checkbox from './Checkbox'; + +test('체크박스 렌더링과 체크 이벤트', async () => { + const onChange = vi.fn(); + render(); + const checkbox = screen.getByRole('checkbox', { name: /동의/i }); + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + expect(onChange).toHaveBeenCalled(); +}); diff --git a/src/componets/Checkbox/Checkbox.tsx b/src/componets/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..62bea45 --- /dev/null +++ b/src/componets/Checkbox/Checkbox.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import './Checkbox.scss'; + +export interface CheckboxProps + extends React.InputHTMLAttributes { + label: string; +} + +const Checkbox: React.FC = ({ label, ...rest }) => { + const reactId = React.useId(); + const id = rest.id ?? reactId; + + return ( + + ); +}; +export default Checkbox; diff --git a/src/styles/_functions.scss b/src/styles/_functions.scss new file mode 100644 index 0000000..0fd84b3 --- /dev/null +++ b/src/styles/_functions.scss @@ -0,0 +1,10 @@ +@use 'sass:map'; +@use 'variables' as v; + +@function color($name) { + $val: map.get(v.$colors, $name); + @if $val == null { + @error "Unknown color token: #{$name}"; + } + @return $val; +} diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 175dd12..b85b052 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -1,22 +1,36 @@ +@use 'sass:map'; @use 'variables' as v; +@use 'functions' as f; -// 반응형 미디어 쿼리 믹스인 +// 반응형 미디어 쿼리 @mixin mq($breakpoint) { - @if map-has-key(v.$breakpoints, $breakpoint) { - @media (min-width: map-get(v.$breakpoints, $breakpoint)) { + @if map.has-key(v.$breakpoints, $breakpoint) { + @media (min-width: map.get(v.$breakpoints, $breakpoint)) { @content; } } } -// 타이포그래피 믹스인 +// 기본 타이포 @mixin text-style($size, $weight: 400) { - font-size: map-get(map-get(v.$typography, $size), fontSize); - line-height: map-get(map-get(v.$typography, $size), lineHeight); + $scale: map.get(v.$typography, $size); + @if $scale == null { + @error "Unknown typography scale: #{$size}"; + } + font-size: map.get($scale, fontSize); + line-height: map.get($scale, lineHeight); font-weight: $weight; } -// flex 믹스인 +// 확장 타이포 (색상) +@mixin text-style-extended($size, $weight: 400, $color: null) { + @include text-style($size, $weight); + @if $color != null { + color: f.color($color); + } +} + +// flex @mixin flex-box($justify: center, $align: center, $direction: row) { display: flex; flex-direction: $direction; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 86ae1ca..78f5b1c 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -6,9 +6,16 @@ $colors: ( gray-300: #e0e0e0, gray-400: #bdbdbd, gray-500: #9e9e9e, + gray-600: #757575, + gray-700: #616161, + gray-800: #424242, gray-900: #212121, + + // 빨강 버튼 red_btn: #d43c3c, - red_btn_hover: #b92a2a + red_btn_active: #a12222, + red_btn_hover: #b92a2a, + red_btn_shadow: #7a1818 ); // 타이포그래피 변수 diff --git a/src/styles/index.scss b/src/styles/index.scss index 38f91cd..e5bf6e6 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,2 +1,3 @@ -@forward "variables"; -@forward "mixins"; +@forward 'variables'; +@forward 'functions'; +@forward 'mixins';