diff --git a/src/components/common/Button/index.tsx b/src/components/common/Button/index.tsx index 381a7bb..3b7ad80 100644 --- a/src/components/common/Button/index.tsx +++ b/src/components/common/Button/index.tsx @@ -3,10 +3,9 @@ import styled from '@emotion/styled'; import React, { ComponentPropsWithoutRef } from 'react'; import { SizeType } from '@/types/size'; +import { SpecialVariantType } from '@/types/variant'; -type ButtonVariantType = 'primary' | 'secondary' | 'warning' | 'danger' | 'outline' | 'ghost'; - -const ButtonVariantStyles = ({ theme, variant }: { theme: Theme; variant: ButtonVariantType }) => { +const ButtonVariantStyles = ({ theme, variant }: { theme: Theme; variant: SpecialVariantType }) => { switch (variant) { case 'secondary': return css` @@ -104,7 +103,7 @@ export interface ButtonVariantProps { /* Button variant */ - variant?: ButtonVariantType; + variant?: SpecialVariantType; /* Button size */ diff --git a/src/components/common/Checkbox/index.tsx b/src/components/common/Checkbox/index.tsx index 77be4ff..87d2886 100644 --- a/src/components/common/Checkbox/index.tsx +++ b/src/components/common/Checkbox/index.tsx @@ -62,7 +62,7 @@ const Checkbox = React.forwardRef( )} {label && {label}} - {children &&
{children}
} + {children} ); }, diff --git a/src/components/common/Switch/index.tsx b/src/components/common/Switch/index.tsx new file mode 100644 index 0000000..b5bc95c --- /dev/null +++ b/src/components/common/Switch/index.tsx @@ -0,0 +1,213 @@ +import { Theme, css } from '@emotion/react'; +import styled from '@emotion/styled'; +import * as React from 'react'; + +import { composeEventHandlers } from '@/libs/event'; +import { SpecialSizeType } from '@/types/size'; +import { VariantType } from '@/types/variant'; + +interface SwitchProps extends Omit, 'size'> { + /* + Switch checked value + */ + checked?: boolean; + /* + Switch Label (Like Description) + */ + label?: string; + /* + Switch variant + */ + variant?: VariantType; + /* + Switch size + */ + size?: SpecialSizeType; + /* + Switch align style (css 'algin-items') + */ + align?: React.CSSProperties['alignItems']; +} + +export const Switch = React.forwardRef( + ( + { + className, + label, + checked = false, + variant = 'primary', + size = 'md', + align = 'center', + disabled = false, + onChange, + id, + children, + ...props + }, + ref, + ) => { + const [chk, setChk] = React.useState(checked); + + React.useEffect(() => { + setChk(checked); + }, [checked]); + + const onChangeHandler = (e: React.ChangeEvent) => { + setChk(e.target.checked); + }; + + return ( + + + + {label && {label}} + {children} + + ); + }, +); +Switch.displayName = 'Switch'; + +const SwitchContainer = styled.label<{ align: React.CSSProperties['alignItems']; disabled: boolean }>` + position: relative; + display: inline-flex; + align-items: ${props => props.align}; + line-height: 1; + ${props => props.disabled && 'opacity: 0.5;'} +`; + +const HiddenSwitch = styled.input` + clip: rect(0 0 0 0); + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0px; + border: none; + position: absolute; + white-space: nowrap; +`; + +const SwitchLabel = styled.span` + font-weight: 500; + margin-left: 1rem; +`; + +const SwitchVariantStyles = ({ theme, variant, checked }: { theme: Theme; variant: VariantType; checked: boolean }) => { + if (!checked) return; + + switch (variant) { + case 'secondary': + return css` + background-color: ${theme.colors.secondary}; + `; + case 'danger': + return css` + color: ${theme.colors.white}; + background-color: ${theme.colors.danger}; + `; + case 'warning': + return css` + color: ${theme.colors.white}; + background-color: ${theme.colors.warning}; + `; + case 'primary': + default: + return css` + color: white; + background-color: ${theme.colors.primary}; + `; + } +}; + +const SwitchSizeStyles = ({ size, checked }: { size: SpecialSizeType; checked: boolean }) => { + switch (size) { + case 'sm': + return css` + width: 2rem; + height: 1rem; + &:after { + top: 0.1rem; + left: 0.1rem; + width: 0.8rem; + height: 0.8rem; + ${checked && `left: calc(100% - 0.1rem);`} + } + `; + case 'lg': + return css` + width: 4rem; + height: 2rem; + &:after { + top: 0.2rem; + left: 0.2rem; + width: 1.6rem; + height: 1.6rem; + ${checked && `left: calc(100% - 0.2rem);`} + } + `; + case 'xl': + return css` + width: 6rem; + height: 3rem; + &:after { + top: 0.3rem; + left: 0.3rem; + width: 2.4rem; + height: 2.4rem; + ${checked && `left: calc(100% - 0.4rem);`} + } + `; + case 'md': + default: + return css` + width: 3rem; + height: 1.5rem; + &:after { + top: 0.15rem; + left: 0.15rem; + width: 1.2rem; + height: 1.2rem; + ${checked && `left: calc(100% - 0.15rem);`} + } + `; + } +}; + +const StyledSwitch = styled.div` + position: relative; + display: inline-block; + border-radius: 50rem; + background-color: ${({ theme }) => theme.colors.gray['200']}; + transition: background-color ease 0.2s; + cursor: pointer; + text-indent: -9999px; + + &:after { + content: ''; + position: absolute; + background: #fff; + border-radius: 50rem; + transition: 0.2s; + } + + ${props => + props.checked && + css` + &:after { + transform: translateX(-100%); + } + `} + + ${({ theme, variant, checked = false }) => variant && SwitchVariantStyles({ theme, variant, checked })} + ${({ size, checked = false }) => size && SwitchSizeStyles({ size, checked })} + ${props => props.disabled && 'cursor: not-allowed;'} +`; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index b0d6377..e96730c 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -8,3 +8,4 @@ export * from './Label'; export * from './Textarea'; export * from './Tooltip'; export * from './Checkbox'; +export * from './Switch'; diff --git a/src/stories/common/Switch.stories.tsx b/src/stories/common/Switch.stories.tsx new file mode 100644 index 0000000..47397c7 --- /dev/null +++ b/src/stories/common/Switch.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Label, Switch } from '../..'; + +const meta = { + title: 'common/Switch', + component: Switch, + tags: ['autodocs'], + argTypes: {}, + parameters: { + componentSubtitle: 'Base Switch', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + checked: true, + label: 'Switch', + }, +}; + +export const WithChildren: Story = { + args: { + id: 'switch-with-child', + label: 'With Children', + }, +}; + +export const WithLabel: Story = { + args: { + id: 'switch-with-text', + }, + decorators: [ + Story => { + return ( +
+ + +
+ ); + }, + ], +}; + +export const Disabled: Story = { + args: { + checked: true, + label: 'Disabled', + disabled: true, + }, +}; diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 0a4b8a8..fe89a02 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -5,7 +5,7 @@ export const BMateColors = { white: '#FAFAFA', black: '#212121', primary: '#212121', - secondary: '#EEEEEE', + secondary: '#E0E0E0', gray: { 50: '#FAFAFA', 100: '#F5F5F5', diff --git a/src/types/size.ts b/src/types/size.ts index 44232e8..aba854d 100644 --- a/src/types/size.ts +++ b/src/types/size.ts @@ -1 +1,2 @@ export type SizeType = 'sm' | 'md' | 'lg'; +export type SpecialSizeType = SizeType | 'xl'; diff --git a/src/types/variant.ts b/src/types/variant.ts new file mode 100644 index 0000000..0ae5b1b --- /dev/null +++ b/src/types/variant.ts @@ -0,0 +1,2 @@ +export type VariantType = 'primary' | 'secondary' | 'warning' | 'danger'; +export type SpecialVariantType = VariantType | 'outline' | 'ghost';