diff --git a/.github/PULL_REQUEST_TEMPLATE/solo-template.yml b/.github/PULL_REQUEST_TEMPLATE/solo-template.yml index 8779fb3..0c7abf7 100644 --- a/.github/PULL_REQUEST_TEMPLATE/solo-template.yml +++ b/.github/PULL_REQUEST_TEMPLATE/solo-template.yml @@ -1,6 +1,6 @@ name: 'πŸ“ PR Template (Solo)' description: '혼자 μ“°λŠ” μ»΄ν¬λ„ŒνŠΈ PR ν…œν”Œλ¦Ώ' -labels: ['pr'] +labels: ['feat'] body: - type: input id: title diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..7438221 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +ignoredBuiltDependencies: + - unrs-resolver + +packages: + - 'xp-components' \ No newline at end of file diff --git a/src/componets/Button/Button.scss b/src/componets/Button/Button.scss index 5107506..182d534 100644 --- a/src/componets/Button/Button.scss +++ b/src/componets/Button/Button.scss @@ -2,7 +2,7 @@ @use 'sass:map'; .btn { - background: #d4d4d4; + background: s.color(gray-300); @include s.text-style-extended(lg, 400, gray-900); border: none; padding: 6px 18px; @@ -25,11 +25,11 @@ 2px 2px 0 0 s.color(gray-900); } &:active { - background: #bbbbbb; + background: s.color(gray-300); color: s.color(gray-900); box-shadow: inset 1px 1px 0 0 s.color(white), - 1px 1px 0 0 #868686, + 1px 1px 0 0 s.color(gray-600), 0px 0px 0 0 s.color(gray-900); } diff --git a/src/componets/Checkbox/Checkbox.scss b/src/componets/Checkbox/Checkbox.scss index bece911..3b95ecc 100644 --- a/src/componets/Checkbox/Checkbox.scss +++ b/src/componets/Checkbox/Checkbox.scss @@ -36,9 +36,9 @@ height: 18px; border-radius: 0; border: 2px solid s.color(gray-400); - background: s.color(white); + background: s.color(gray-300); cursor: pointer; - transition: + transition: background-color 0.2s, border-color 0.2s; @@ -46,6 +46,7 @@ ~ .checkbox_png { opacity: 1; } + border: 2px solid s.color(gray-900); } @media (max-width: 640px) { diff --git a/src/componets/Checkbox/Checkbox.stories.tsx b/src/componets/Checkbox/Checkbox.stories.tsx index 2a3cf79..4c672ff 100644 --- a/src/componets/Checkbox/Checkbox.stories.tsx +++ b/src/componets/Checkbox/Checkbox.stories.tsx @@ -10,13 +10,13 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Primary: Story = { +export const Default: Story = { args: { label: 'Checkbox Default' } }; -export const Secondary: Story = { +export const Checked: Story = { args: { label: 'Checkbox Checked', checked: true diff --git a/src/componets/Input/Input.scss b/src/componets/Input/Input.scss new file mode 100644 index 0000000..3194132 --- /dev/null +++ b/src/componets/Input/Input.scss @@ -0,0 +1,77 @@ +@use '../../styles/index' as s; +@use 'sass:map'; + + +.input_wrapper { + position: relative; + cursor: pointer; + width: 100%; +} + +// 인풋 ν•„λ“œ +.input_field { + width: 100%; + padding: 10px; + box-sizing: border-box; + border: none; + background: s.color(gray-300); + color: s.color(gray-900); + + box-shadow: + 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, + box-shadow 0.12s; + + + &:hover, + &:focus { + outline: none; + background: s.color(gray-300); + color: s.color(gray-900); + box-shadow: + inset 1px 1px 0 0 s.color(white), + 2px 2px 0 0 s.color(gray-900); + } + + &:active { + background: s.color(gray-300); + color: s.color(gray-900); + box-shadow: + inset 1px 1px 0 0 s.color(white), + 1px 1px 0 0 s.color(gray-600), + 0px 0px 0 0 s.color(gray-900); + } + + @media (max-width: 640px) { + @include s.text-style-extended(md, 400, gray-900); + padding: 5px 13px; + } +} + +// placeholder μŠ€νƒ€μΌ +.input_placeholder { + position: absolute; + left: 8px; + top: -10px; + @include s.text-style-extended(xs, 400, gray-900); + background: s.color(white); + padding: 0 4px; + pointer-events: none; + transition: 0.2s; + box-shadow: + inset 1px 1px 0 0 s.color(white), + 1px 1px 0 0 s.color(gray-600), + 0px 0px 0 0 s.color(gray-900); +} + +// focus μ‹œ placeholder μŠ€νƒ€μΌ +.input_field:focus + .input_placeholder, +.input_field:not(:placeholder-shown) + .input_placeholder { + + background: s.color(white); + padding: 0 4px; +} \ No newline at end of file diff --git a/src/componets/Input/Input.stories.tsx b/src/componets/Input/Input.stories.tsx new file mode 100644 index 0000000..ee0b306 --- /dev/null +++ b/src/componets/Input/Input.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import Input from './Input'; + +const meta: Meta = { + title: 'Components/Input', + component: Input, + argTypes: { onChange: { action: 'changed' } } +}; + +export default meta; +type Story = StoryObj; + +export const Text: Story = { + args: { + label: 'Text μž…λ ₯', + id: 'input-text', + type: 'text', + placeholder: 'ν…μŠ€νŠΈ μž…λ ₯' + } +}; + +export const Password: Story = { + args: { + label: 'λΉ„λ°€λ²ˆν˜Έ μž…λ ₯', + id: 'input-password', + type: 'password', + placeholder: 'λΉ„λ°€λ²ˆν˜Έ μž…λ ₯' + } +}; + +export const Email: Story = { + args: { + label: '이메일 μž…λ ₯', + id: 'input-email', + type: 'email', + placeholder: '이메일 μž…λ ₯' + } +}; + +export const AutoId: Story = { + args: { + label: 'μžλ™ ID Input', + placeholder: 'id 없이 λ Œλ”λ§' + } +}; \ No newline at end of file diff --git a/src/componets/Input/Input.test.tsx b/src/componets/Input/Input.test.tsx new file mode 100644 index 0000000..aa9dd60 --- /dev/null +++ b/src/componets/Input/Input.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { test, expect, vi } from 'vitest'; + +import Input from './Input'; + +test('label(ν”Œλ ˆμ΄μŠ€ν™€λ”)κ³Ό input이 id둜 μ—°κ²°λ˜μ–΄ μžˆλ‹€', () => { + render(); + const label = screen.getByText('ν…ŒμŠ€νŠΈλΌλ²¨'); + const input = screen.getByRole('textbox'); + expect(label).toHaveAttribute('for', input.getAttribute('id')); +}); + +test('type, placeholder, value props 전달 확인', () => { + render( + + ); + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('type', 'email'); + expect(input).toHaveAttribute('placeholder', '이메일 μž…λ ₯'); + expect(input).toHaveValue('abc@ex.com'); +}); + +test('Input μž…λ ₯ μ‹œ onChange 이벀트 λ°œμƒ', async () => { + const handleChange = vi.fn(); + render(); + const input = screen.getByLabelText('νŒ¨μŠ€μ›Œλ“œ'); + await userEvent.type(input, 'secret'); + expect(handleChange).toHaveBeenCalled(); +}); + +test('id μ†Œν’ˆ 없을 λ•Œ μžλ™ id 생성', () => { + render(); + const input = screen.getByRole('textbox'); + const label = screen.getByText('μžλ™ID'); + expect(input.getAttribute('id')).toBeTruthy(); + expect(label).toHaveAttribute('for', input.getAttribute('id')); +}); \ No newline at end of file diff --git a/src/componets/Input/Input.tsx b/src/componets/Input/Input.tsx new file mode 100644 index 0000000..4bd7d86 --- /dev/null +++ b/src/componets/Input/Input.tsx @@ -0,0 +1,21 @@ +import React, { useId } from 'react'; +import './Input.scss'; + +export interface InputProps + extends React.InputHTMLAttributes { + label: string; +} + +const Input: React.FC = ({ label, id: propId, type = 'text', ...rest }) => { + const reactId = useId(); + const id = propId || reactId; + + return ( +
+ + +
+ ); +}; + +export default Input; \ No newline at end of file diff --git a/src/public/vite.svg b/src/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/src/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file