diff --git a/packages/design-system/packages/core/src/components/index.scss b/packages/design-system/packages/core/src/components/index.scss index d4eca685..f7f703e2 100644 --- a/packages/design-system/packages/core/src/components/index.scss +++ b/packages/design-system/packages/core/src/components/index.scss @@ -4,3 +4,4 @@ @import 'switch'; @import 'chip'; @import 'text-input'; +@import 'search-input'; diff --git a/packages/design-system/packages/core/src/components/index.ts b/packages/design-system/packages/core/src/components/index.ts index 4d5bc403..cfa58f33 100644 --- a/packages/design-system/packages/core/src/components/index.ts +++ b/packages/design-system/packages/core/src/components/index.ts @@ -6,3 +6,4 @@ export * from './switch' export * from './chip' export * from './text-input' export * from './hidden-text' +export * from './search-input' diff --git a/packages/design-system/packages/core/src/components/search-input/SearchInput.stories.tsx b/packages/design-system/packages/core/src/components/search-input/SearchInput.stories.tsx new file mode 100644 index 00000000..52de3c45 --- /dev/null +++ b/packages/design-system/packages/core/src/components/search-input/SearchInput.stories.tsx @@ -0,0 +1,101 @@ +/* eslint-disable no-alert */ +/* eslint-disable tailwindcss/no-custom-classname */ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import SearchInput from './SearchInput' +import { $variable } from '../../token' + +const meta: Meta = { + title: 'Inputs/SearchInput', + component: SearchInput, + tags: ['autodocs'], + parameters: { + componentSubtitle: 'Attraction에서 사용되는 검색 인풋 컴포넌트입니다.', + }, + argTypes: { + size: { + description: '인풋의 크기를 지정합니다.', + control: 'select', + options: ['md', 'lg'], + table: { + type: { summary: ['md', 'lg'].join(' | ') }, + defaultValue: { summary: 'md' }, + }, + }, + round: { + description: '인풋의 모서리 형태를 지정합니다.', + control: 'select', + options: ['xs', 'sm', 'md', 'lg', 'full'], + table: { + type: { summary: ['xs', 'sm', 'md', 'lg', 'full'].join(' | ') }, + defaultValue: { summary: 'sm' }, + }, + }, + disabled: { + description: '인풋의 비활성화 상태를 지정합니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + withBackground: { + description: '인풋 배경색의 기본 상태를 지정합니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + withClearButton: { + description: '검색어를 지우는 버튼을 렌더링합니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + placeholder: { + description: '인풋의 placeholder를 지정합니다.', + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' }, + }, + }, + }, +} + +export default meta + +type Story = StoryObj + +const style: React.CSSProperties = { + display: 'block', + padding: '30px', + width: '50%', +} + +export const SearchInputDefault: Story = { + render: (props) => ( +
+
+ +
+
+ +
+
+ ), + args: { + onEnter: () => alert('on enter !!!'), + onClear: () => alert('on clear !!!'), + }, +} diff --git a/packages/design-system/packages/core/src/components/search-input/SearchInput.style.ts b/packages/design-system/packages/core/src/components/search-input/SearchInput.style.ts new file mode 100644 index 00000000..efc9ca5f --- /dev/null +++ b/packages/design-system/packages/core/src/components/search-input/SearchInput.style.ts @@ -0,0 +1,29 @@ +import { cva } from 'class-variance-authority' + +const searchInputClassName = 'ds-search-input' +const getSearchInputModifier = (name: string) => { + return `${searchInputClassName}--${name}` +} + +export const variants = { + size: { + md: '', + lg: getSearchInputModifier('size-lg'), + }, + round: { + xs: getSearchInputModifier('round-xs'), + sm: '', + md: getSearchInputModifier('round-md'), + lg: getSearchInputModifier('round-lg'), + full: getSearchInputModifier('round-full'), + }, + background: { + none: '', + with: getSearchInputModifier('background'), + }, +} + +export const searchInputVariants = cva(searchInputClassName, { + variants, + defaultVariants: { size: 'md', round: 'sm', background: 'none' }, +}) diff --git a/packages/design-system/packages/core/src/components/search-input/SearchInput.tsx b/packages/design-system/packages/core/src/components/search-input/SearchInput.tsx index 57336f77..8d0ec927 100644 --- a/packages/design-system/packages/core/src/components/search-input/SearchInput.tsx +++ b/packages/design-system/packages/core/src/components/search-input/SearchInput.tsx @@ -1,5 +1,75 @@ import React from 'react' +import { BackspaceOutline, MagnifyingGlassOutline } from '@attraction/icons' +import { cn } from '@attraction/utils' +import { HiddenText } from '../hidden-text' +import { searchInputVariants, variants } from './SearchInput.style' -const SearchInput = React.forwardRef(() => ) +type SearchInputVariant = typeof variants +interface SearchInputProps + extends Omit< + React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + >, + 'size' + > { + size?: keyof SearchInputVariant['size'] + round?: keyof SearchInputVariant['round'] + withBackground?: boolean + onEnter?: React.KeyboardEventHandler + withClearButton?: boolean + onClear?: React.MouseEventHandler +} + +const SearchInput = React.forwardRef( + ( + { + className, + style, + placeholder, + onKeyDown, + size, + round, + withBackground, + onEnter, + withClearButton, + onClear, + ...props + }, + ref, + ) => ( +
+ + { + if (e.key === 'Enter') { + onEnter?.(e) + } + onKeyDown?.(e) + }} + {...props} + /> + {withClearButton && ( + + )} +
+ ), +) export default SearchInput diff --git a/packages/design-system/packages/core/src/components/search-input/_index.scss b/packages/design-system/packages/core/src/components/search-input/_index.scss new file mode 100644 index 00000000..18777952 --- /dev/null +++ b/packages/design-system/packages/core/src/components/search-input/_index.scss @@ -0,0 +1,185 @@ +@use '../../token/variable'; + +.ds-search-input { + position: relative; + display: block; + width: 100%; + height: auto; + + & > svg { + position: absolute; + top: 0px; + bottom: 0px; + left: 0.625rem; + width: variable.$ds-font-size-400; + height: variable.$ds-font-size-400; + box-sizing: border-box; + padding: 0px; + margin: auto; + font-size: variable.$ds-font-size-400; + color: variable.$ds-gray-500; + .dark & { + color: variable.$ds-gray-400; + } + } + + & > input { + display: block; + width: 100%; + height: 2.5rem; + box-sizing: border-box; + padding: 0.5rem 2.5rem; + border: 1px solid variable.$ds-gray-100; + font-size: variable.$ds-font-size-300; + font-weight: variable.$ds-font-weight-regular; + color: variable.$ds-gray-700; + border-radius: 0.5rem; + outline: none; + background-color: variable.$ds-gray-000; + @include variable.transition-colors(); + + .dark & { + color: variable.$ds-gray-050; + border-color: variable.$ds-gray-700; + background-color: variable.$ds-gray-800; + } + + &::placeholder { + color: variable.$ds-gray-500; + .dark & { + color: variable.$ds-gray-400; + } + } + &:disabled { + color: variable.$ds-gray-400 !important; + background-color: variable.$ds-gray-050 !important; + cursor: not-allowed; + .dark & { + color: variable.$ds-gray-500 !important; + background-color: variable.$ds-gray-700 !important; + } + } + + &:focus, + .dark &:focus { + border-color: variable.$ds-blue-400; + } + + &::-ms-clear, + &::-ms-reveal { + display: none; + width: 0; + height: 0; + } + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + display: none; + } + } + + & > button { + position: absolute; + top: 0px; + bottom: 0px; + right: 0px; + display: inline-flex; + justify-content: center; + align-items: center; + width: 2.5rem; + height: 2.5rem; + box-sizing: border-box; + margin: auto; + border: none; + color: variable.$ds-gray-500; + border-radius: 0.25rem; + background-color: transparent; + cursor: pointer; + .dark & { + color: variable.$ds-gray-400; + } + &:disabled { + color: variable.$ds-gray-400; + cursor: not-allowed; + .dark & { + color: variable.$ds-gray-500; + } + } + & > svg { + width: variable.$ds-font-size-500; + height: variable.$ds-font-size-500; + font-size: variable.$ds-font-size-500; + color: inherit; + } + } + + &--background > input { + background-color: variable.$ds-gray-050; + .dark & { + background-color: variable.$ds-gray-700; + } + &:focus { + background-color: variable.$ds-gray-000; + .dark & { + background-color: variable.$ds-gray-800; + } + } + } + + &--round-xs > input { + border-radius: 0.25rem; + } + &--round-md > input { + border-radius: 0.75rem; + } + &--round-lg > input { + border-radius: 1rem; + } + &--round-full { + & > svg { + left: 0.75rem; + } + & > input { + padding: 0.5rem 2.625rem; + border-radius: 9999px; + } + & > button { + right: 0.25rem; + border-radius: 9999px; + } + } + + &--size-lg { + & > svg { + left: 0.875rem; + width: variable.$ds-font-size-500; + height: variable.$ds-font-size-500; + font-size: variable.$ds-font-size-500; + } + & > input { + height: 3rem; + padding: 0.5rem 2.75rem; + font-size: variable.$ds-font-size-400; + } + & > button { + right: 0.25rem; + & > svg { + width: variable.$ds-font-size-600; + height: variable.$ds-font-size-600; + font-size: variable.$ds-font-size-600; + } + } + &.ds-search-input--round-full { + & > svg { + left: 0.875rem; + } + & > input { + padding: 0.5rem 2.75rem; + } + & > button { + right: 0.375rem; + } + } + } +}