From 52f8c30b2ef21bd0c3b120f59eeef5033e0a5592 Mon Sep 17 00:00:00 2001 From: "JoohyunKim(Lucy)" Date: Mon, 13 Apr 2026 15:27:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/breadcrumbs/Breadcrumbs.tsx | 58 ++++++++++ .../shared/components/breadcrumbs/index.ts | 5 + .../shared/components/checkbox/Checkbox.tsx | 66 ++++++++++++ .../descriptions/items/DescriptionsItem.tsx | 2 +- .../shared/components/icons/OverlayArrow.tsx | 9 ++ web-app/app/shared/components/icons/index.ts | 1 + web-app/app/shared/components/radio/Radio.tsx | 102 ++++++++++++++++++ .../components/radioGroup/RadioGroup.tsx | 77 +++++++++++++ .../app/shared/components/switch/Switch.tsx | 59 ++++++++++ web-app/app/shared/components/tab/Tab.tsx | 55 ++++++++++ .../shared/components/textarea/TextArea.tsx | 21 ++++ .../app/shared/components/textarea/index.ts | 1 + .../app/shared/components/tooltip/Tooltip.tsx | 44 ++++++++ .../app/shared/components/tooltip/index.ts | 1 + 14 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx create mode 100644 web-app/app/shared/components/breadcrumbs/index.ts create mode 100644 web-app/app/shared/components/checkbox/Checkbox.tsx create mode 100644 web-app/app/shared/components/icons/OverlayArrow.tsx create mode 100644 web-app/app/shared/components/radio/Radio.tsx create mode 100644 web-app/app/shared/components/radioGroup/RadioGroup.tsx create mode 100644 web-app/app/shared/components/switch/Switch.tsx create mode 100644 web-app/app/shared/components/tab/Tab.tsx create mode 100644 web-app/app/shared/components/textarea/TextArea.tsx create mode 100644 web-app/app/shared/components/textarea/index.ts create mode 100644 web-app/app/shared/components/tooltip/Tooltip.tsx create mode 100644 web-app/app/shared/components/tooltip/index.ts diff --git a/web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx b/web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000..519356d --- /dev/null +++ b/web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,58 @@ +import type { FC } from 'react'; +import { Breadcrumb as AriaBreadcrumb, Breadcrumbs as AriaBreadcrumbs } from 'react-aria-components'; + +export interface BreadcrumbsProps { + items: string[]; +} +const Breadcrumbs: FC = (props) => { + const { items } = props; + + return ( + + ); +}; + +export default Breadcrumbs; diff --git a/web-app/app/shared/components/breadcrumbs/index.ts b/web-app/app/shared/components/breadcrumbs/index.ts new file mode 100644 index 0000000..66298ba --- /dev/null +++ b/web-app/app/shared/components/breadcrumbs/index.ts @@ -0,0 +1,5 @@ +import type { BreadcrumbsProps } from './Breadcrumbs'; +import InternalBreadcrumbs from './Breadcrumbs'; + +export type { BreadcrumbsProps }; +export const Breadcrumbs = InternalBreadcrumbs; diff --git a/web-app/app/shared/components/checkbox/Checkbox.tsx b/web-app/app/shared/components/checkbox/Checkbox.tsx new file mode 100644 index 0000000..d94c0e2 --- /dev/null +++ b/web-app/app/shared/components/checkbox/Checkbox.tsx @@ -0,0 +1,66 @@ +import type { CheckboxProps as AriaCheckboxProps } from 'react-aria-components'; +import { Checkbox as AriaCheckbox, composeRenderProps } from 'react-aria-components'; + +import { tv } from 'tailwind-variants'; + +const checkbox = tv({ + base: 'inline-flex items-center gap-2 text-sm font-medium leading-7', + variants: { + isDisabled: { + true: 'text-dabeeo-gray-be', + }, + }, +}); + +const box = tv({ + base: 'w-4 h-4 box-border shrink-0 flex items-center justify-center text-white border border-dabeeo-gray-be transition', + variants: { + isSelected: { + true: 'bg-primary border-primary', + }, + isDisabled: { + true: 'text-dabeeo-gray-99 bg-dabeeo-gray-eb border-dabeeo-gray-be cursor-not-allowed', + }, + isFocusVisible: { + true: 'ring-2 ring-offset-2 ring-primary', + }, + }, + compoundVariants: [], +}); + +export const Checkbox = (props: AriaCheckboxProps) => { + const { className, children, ...restProps } = props; + return ( + checkbox({ ...renderProps, className }))} + {...restProps} + > + {composeRenderProps(children, (children, { isSelected, isIndeterminate, ...renderProps }) => ( + <> +
+ {isIndeterminate ? ( + + + + ) : isSelected ? ( + + + + ) : null} +
+ {children} + + ))} +
+ ); +}; diff --git a/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx b/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx index eb53f00..c7cc221 100644 --- a/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx +++ b/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx @@ -26,7 +26,7 @@ export const DescriptionsItem = (props: DescriptionsItemProps) => {
) { + return ( + + + + ); +} diff --git a/web-app/app/shared/components/icons/index.ts b/web-app/app/shared/components/icons/index.ts index 7df7603..59d4a79 100644 --- a/web-app/app/shared/components/icons/index.ts +++ b/web-app/app/shared/components/icons/index.ts @@ -2,4 +2,5 @@ export * from './Calendar'; export * from './ChevronLeft'; export * from './ChevronRight'; export * from './LoadingSpinner'; +export * from './OverlayArrow'; diff --git a/web-app/app/shared/components/radio/Radio.tsx b/web-app/app/shared/components/radio/Radio.tsx new file mode 100644 index 0000000..addcb58 --- /dev/null +++ b/web-app/app/shared/components/radio/Radio.tsx @@ -0,0 +1,102 @@ +import { type ReactNode, useId } from 'react'; + +import { tv } from 'tailwind-variants'; + +export interface RadioProps { + name: string; + value: string; + checked?: boolean; + defaultChecked?: boolean; + onChange?: (value: string) => void; + disabled?: boolean; + readOnly?: boolean; + children?: ReactNode; + className?: string; +} + +const radioStyles = tv({ + base: [ + 'shrink-0 w-4 h-4 min-w-4 min-h-4 box-border rounded-full', + 'border-2 border-dabeeo-gray-be bg-white', + 'flex items-center justify-center', + 'transition-all duration-200', + 'peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-primary', + ], + variants: { + isSelected: { + true: 'border-primary bg-primary', + }, + isDisabled: { + true: 'border-dabeeo-gray-be bg-dabeeo-gray-eb', + }, + isReadOnly: { + true: '', + }, + }, + compoundVariants: [ + { + isSelected: true, + isDisabled: true, + className: 'border-dabeeo-gray-be bg-dabeeo-gray-eb', + }, + ], +}); + +const innerCircleStyles = tv({ + base: 'w-2 h-2 rounded-full transition-all duration-200', + variants: { + isSelected: { + true: 'bg-white', + false: 'bg-transparent', + }, + isDisabled: { + true: 'bg-dabeeo-gray-eb', + }, + }, + compoundVariants: [ + { + isSelected: true, + isDisabled: true, + className: 'bg-dabeeo-gray-eb', + }, + ], +}); + +const labelStyles = tv({ + base: 'text-dabeeo-black-22 text-sm font-medium inline-flex items-center', + variants: { + isDisabled: { + true: 'text-dabeeo-gray-be', + }, + }, +}); + +export const Radio = (props: RadioProps) => { + const { name, value, checked, defaultChecked, onChange, disabled, readOnly, children, className } = props; + const id = useId(); + const isInteractive = !disabled && !readOnly; + + return ( + + ); +}; diff --git a/web-app/app/shared/components/radioGroup/RadioGroup.tsx b/web-app/app/shared/components/radioGroup/RadioGroup.tsx new file mode 100644 index 0000000..fe4f13e --- /dev/null +++ b/web-app/app/shared/components/radioGroup/RadioGroup.tsx @@ -0,0 +1,77 @@ +import type { ReactNode } from 'react'; +import { useId } from 'react'; +import type { RadioGroupProps as AriaRadioGroupProps, RadioProps as AriaRadioProps } from 'react-aria-components'; +import { Radio as AriaRadio, RadioGroup as AriaRadioGroup, composeRenderProps } from 'react-aria-components'; + +import clsx from 'clsx'; +import { tv } from 'tailwind-variants'; + +export type RadioOption = { + value: string; + label: ReactNode; + isDisabled?: boolean; +}; + +export interface RadioGroupProps extends AriaRadioGroupProps { + items: RadioOption[]; +} + +export const RadioGroup = (props: RadioGroupProps) => { + const radioGroupId = useId(); + const { items, className, value, ...restProps } = props; + return ( + + {items.map((item) => { + return ( + + {item.label} + + ); + })} + + ); +}; + +interface RadioProps extends AriaRadioProps {} + +const radioStyles = tv({ + base: 'shrink-0 w-4 h-4 box-border rounded-full border-[1.5px] border-dabeeo-gray-be bg-white transition-all', + variants: { + isSelected: { + true: 'border-4 border-primary ', + }, + isFocusVisible: { + true: 'ring-2 ring-offset-2 ring-primary', + }, + isInvalid: { + true: 'border-dabeeo-red', + }, + isDisabled: { + true: 'border-dabeeo-gray-be', + }, + }, + compoundVariants: [{ isDisabled: true, isSelected: false, className: 'bg-dabeeo-gray-eb' }], +}); +export const Radio = (props: RadioProps) => { + const { className, ...restProps } = props; + return ( + + {composeRenderProps(props.children, (children, renderProps) => ( + <> +
+ {children} + + ))} + + ); +}; diff --git a/web-app/app/shared/components/switch/Switch.tsx b/web-app/app/shared/components/switch/Switch.tsx new file mode 100644 index 0000000..a95e33f --- /dev/null +++ b/web-app/app/shared/components/switch/Switch.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Switch as AriaSwitch, SwitchProps as AriaSwitchProps } from 'react-aria-components'; + +import clsx from 'clsx'; +import { tv } from 'tailwind-variants'; + +export interface SwitchProps extends Omit { + children?: React.ReactNode; +} + +const track = tv({ + base: 'flex h-4.5 w-8 box-border px-1 items-center rounded-full transition duration-200 ease-in-out', + variants: { + isSelected: { + false: 'bg-dabeeo-gray-99', + true: 'bg-dabeeo-green-main', + }, + isDisabled: { + true: 'bg-dabeeo-gray-eb', + }, + isReadOnly: { + true: 'opacity-50', + }, + isFocusVisible: { + true: 'ring-2 ring-offset-2 ring-primary', + }, + }, +}); + +const handle = tv({ + base: 'h-3 w-3 transform rounded-full outline outline-1 -outline-offset-1 outline-transparent shadow-[0_1px_3px_0_rgba(5,48,48,0.35)] transition duration-200 ease-in-out', + variants: { + isSelected: { + false: 'translate-x-0 bg-white', + true: 'translate-x-[11px] bg-white', + }, + }, +}); + +export function Switch({ children, ...props }: SwitchProps) { + return ( + + {(renderProps) => ( + <> +
+ +
+ {children} + + )} +
+ ); +} diff --git a/web-app/app/shared/components/tab/Tab.tsx b/web-app/app/shared/components/tab/Tab.tsx new file mode 100644 index 0000000..631d402 --- /dev/null +++ b/web-app/app/shared/components/tab/Tab.tsx @@ -0,0 +1,55 @@ +import type { TabsProps } from 'react-aria-components'; +import { Tab as AriaTab, TabList, TabPanel, TabPanels, Tabs } from 'react-aria-components'; + +import { twMerge } from 'tailwind-merge'; + +type TabItem = { + key: string; + label: React.ReactNode; + children: React.ReactNode; + disabled?: boolean; +}; + +// type TabProps = { +// items: TabItem[]; +// activeKey?: string; +// onClick?: (key: string) => void; +// className?: string; +// }; + +/** + * @desc 탭 컴포넌트 + * @param items 탭 아이템 목록 + * @param activeKey 현재 활성화된 탭의 키 + * @param onClick 탭 변경 이벤트 + * @param className 추가 클래스명 + */ +export interface TabProps extends Omit { + className?: string; + items: TabItem[]; + tabPanelClassName?: string; + tabPanelsClassName?: string; +} +export const Tab = ({ items, className, tabPanelClassName, tabPanelsClassName, ...restProps }: TabProps) => { + return ( + + + {(item) => ( + + {item.label} + + )} + + + {(item) => ( + + {item.children} + + )} + + + ); +}; diff --git a/web-app/app/shared/components/textarea/TextArea.tsx b/web-app/app/shared/components/textarea/TextArea.tsx new file mode 100644 index 0000000..4cbbb22 --- /dev/null +++ b/web-app/app/shared/components/textarea/TextArea.tsx @@ -0,0 +1,21 @@ +import { TextArea as AriaTextArea, TextAreaProps as AriaTextAreaProps } from 'react-aria-components'; + +import { twMerge } from 'tailwind-merge'; + +interface TextAreaProps extends Omit { + className?: string; +} + +export const TextArea = (props: TextAreaProps) => { + const { className, ...restProps } = props; + return ( + + ); +}; diff --git a/web-app/app/shared/components/textarea/index.ts b/web-app/app/shared/components/textarea/index.ts new file mode 100644 index 0000000..4853311 --- /dev/null +++ b/web-app/app/shared/components/textarea/index.ts @@ -0,0 +1 @@ +export * from './TextArea'; diff --git a/web-app/app/shared/components/tooltip/Tooltip.tsx b/web-app/app/shared/components/tooltip/Tooltip.tsx new file mode 100644 index 0000000..7dd0dba --- /dev/null +++ b/web-app/app/shared/components/tooltip/Tooltip.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + Tooltip as AriaTooltip, + TooltipProps as AriaTooltipProps, + OverlayArrow, + composeRenderProps, +} from 'react-aria-components'; + +import { tv } from 'tailwind-variants'; +import { OverlayArrowIcon } from '../icons'; + +const style = tv({ + base: 'group/tooltip px-2 py-1.5 max-w-[200px] drop-shadow-modal rounded text-sm', + variants: { + bgColor: { + white: 'bg-white [&_svg]:fill-white', + tertiary: 'bg-primary-tertiary [&_svg]:fill-primary-tertiary', + }, + }, +}); + +export interface TooltipProps extends Omit { + bgColor?: 'white' | 'tertiary'; + children: React.ReactNode; +} + +export function Tooltip({ children, ...props }: TooltipProps) { + const { className, bgColor = 'white', offset = 10, ...restProps } = props; + + return ( + + style({ ...renderProps, className, bgColor }) + )} + > + + + + {children} + + ); +} diff --git a/web-app/app/shared/components/tooltip/index.ts b/web-app/app/shared/components/tooltip/index.ts new file mode 100644 index 0000000..7594a8f --- /dev/null +++ b/web-app/app/shared/components/tooltip/index.ts @@ -0,0 +1 @@ +export * from './Tooltip';