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';