feature/lucy-components #10
58
web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx
Normal file
58
web-app/app/shared/components/breadcrumbs/Breadcrumbs.tsx
Normal file
@@ -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<BreadcrumbsProps> = (props) => {
|
||||||
|
const { items } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav aria-label="Breadcrumb">
|
||||||
|
<AriaBreadcrumbs className="flex items-center gap-2.5">
|
||||||
|
<AriaBreadcrumb aria-label="홈">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="19"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 19 18"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.75 14.4545H4.25V7.83333L9.25 4L14.25 7.83333V14.4545H11.75"
|
||||||
|
stroke="#6C7789"
|
||||||
|
strokeLinecap="square"
|
||||||
|
/>
|
||||||
|
<path d="M10.8381 12.9615V10.3633H7.65625V12.9615" stroke="#6C7789" />
|
||||||
|
</svg>
|
||||||
|
</AriaBreadcrumb>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<AriaBreadcrumb
|
||||||
|
className="flex items-center gap-2.5 text-xs text-dabeeo-black-47 data-current:text-dabeeo-black-22"
|
||||||
|
key={`breadcrumb-${index}-${item}`}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="6"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 6 8"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.70722 4.00064L0.25 1.54349L1.4556 0.337891L5.12572 4.00064L1.45572 7.66347L0.250117 6.45786L2.70722 4.00064Z"
|
||||||
|
fill="#6C7789"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{item}
|
||||||
|
</>
|
||||||
|
</AriaBreadcrumb>
|
||||||
|
))}
|
||||||
|
</AriaBreadcrumbs>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Breadcrumbs;
|
||||||
5
web-app/app/shared/components/breadcrumbs/index.ts
Normal file
5
web-app/app/shared/components/breadcrumbs/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { BreadcrumbsProps } from './Breadcrumbs';
|
||||||
|
import InternalBreadcrumbs from './Breadcrumbs';
|
||||||
|
|
||||||
|
export type { BreadcrumbsProps };
|
||||||
|
export const Breadcrumbs = InternalBreadcrumbs;
|
||||||
66
web-app/app/shared/components/checkbox/Checkbox.tsx
Normal file
66
web-app/app/shared/components/checkbox/Checkbox.tsx
Normal file
@@ -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 (
|
||||||
|
<AriaCheckbox
|
||||||
|
className={composeRenderProps(className, (className, renderProps) => checkbox({ ...renderProps, className }))}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{composeRenderProps(children, (children, { isSelected, isIndeterminate, ...renderProps }) => (
|
||||||
|
<>
|
||||||
|
<div className={box({ isSelected: isSelected || isIndeterminate, ...renderProps })}>
|
||||||
|
{isIndeterminate ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
) : isSelected ? (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 7.00893L7.85714 10.625L13 5" stroke="currentColor" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</AriaCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,7 +26,7 @@ export const DescriptionsItem = (props: DescriptionsItemProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative flex flex-1 items-center pl-4 pr-3 py-2 text-sm font-medium text-kc-black-22 overflow-x-auto after:content-[\'\'] after:absolute after:left-0 after:right-0 after:bottom-0 after:h-px after:bg-primary-tertiary01',
|
'relative flex flex-1 items-center pl-4 pr-3 py-2 text-sm font-medium text-dabeeo-black-22 overflow-x-auto after:content-[\'\'] after:absolute after:left-0 after:right-0 after:bottom-0 after:h-px after:bg-primary-tertiary01',
|
||||||
contentClassName,
|
contentClassName,
|
||||||
isFirstRow &&
|
isFirstRow &&
|
||||||
'before:content-[\'\'] before:absolute before:left-0 before:right-0 before:top-0 before:h-px before:bg-primary-tertiary01'
|
'before:content-[\'\'] before:absolute before:left-0 before:right-0 before:top-0 before:h-px before:bg-primary-tertiary01'
|
||||||
|
|||||||
9
web-app/app/shared/components/icons/OverlayArrow.tsx
Normal file
9
web-app/app/shared/components/icons/OverlayArrow.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function OverlayArrowIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M4.5 7L0 0H9L4.5 7Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,5 @@ export * from './Calendar';
|
|||||||
export * from './ChevronLeft';
|
export * from './ChevronLeft';
|
||||||
export * from './ChevronRight';
|
export * from './ChevronRight';
|
||||||
export * from './LoadingSpinner';
|
export * from './LoadingSpinner';
|
||||||
|
export * from './OverlayArrow';
|
||||||
|
|
||||||
|
|||||||
102
web-app/app/shared/components/radio/Radio.tsx
Normal file
102
web-app/app/shared/components/radio/Radio.tsx
Normal file
@@ -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 (
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className={`inline-flex h-9 items-center gap-2.5 ${isInteractive ? 'cursor-pointer' : 'cursor-not-allowed'} ${className ?? ''}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onChange={(e) => isInteractive && onChange?.(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
readOnly={readOnly}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<span className={radioStyles({ isSelected: checked, isDisabled: disabled, isReadOnly: readOnly })}>
|
||||||
|
<span className={innerCircleStyles({ isSelected: checked, isDisabled: disabled })} />
|
||||||
|
</span>
|
||||||
|
{children && <span className={labelStyles({ isDisabled: disabled })}>{children}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
77
web-app/app/shared/components/radioGroup/RadioGroup.tsx
Normal file
77
web-app/app/shared/components/radioGroup/RadioGroup.tsx
Normal file
@@ -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 (
|
||||||
|
<AriaRadioGroup
|
||||||
|
className={clsx('flex gap-4 data-[orientation=vertical]:flex-col data-[orientation=vertical]:gap-3', className)}
|
||||||
|
value={value === '' ? null : value}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<Radio key={`radio_group-${radioGroupId}-${item.value}`} value={item.value} isDisabled={item.isDisabled}>
|
||||||
|
{item.label}
|
||||||
|
</Radio>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AriaRadioGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<AriaRadio
|
||||||
|
className={clsx(
|
||||||
|
'inline-flex items-center gap-2.5 text-dabeeo-black-22 text-sm font-medium data-disabled:text-dabeeo-gray-be',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{composeRenderProps(props.children, (children, renderProps) => (
|
||||||
|
<>
|
||||||
|
<div className={radioStyles(renderProps)} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</AriaRadio>
|
||||||
|
);
|
||||||
|
};
|
||||||
59
web-app/app/shared/components/switch/Switch.tsx
Normal file
59
web-app/app/shared/components/switch/Switch.tsx
Normal file
@@ -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<AriaSwitchProps, 'children'> {
|
||||||
|
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 (
|
||||||
|
<AriaSwitch
|
||||||
|
{...props}
|
||||||
|
className={clsx(
|
||||||
|
props.className,
|
||||||
|
'relative flex gap-2 items-center text-dabeeo-black-34 text-sm transition [-webkit-tap-highlight-color:transparent] cursor-pointer'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(renderProps) => (
|
||||||
|
<>
|
||||||
|
<div className={track(renderProps)}>
|
||||||
|
<span className={handle(renderProps)} />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaSwitch>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
web-app/app/shared/components/tab/Tab.tsx
Normal file
55
web-app/app/shared/components/tab/Tab.tsx
Normal file
@@ -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<TabsProps, 'className' | 'children'> {
|
||||||
|
className?: string;
|
||||||
|
items: TabItem[];
|
||||||
|
tabPanelClassName?: string;
|
||||||
|
tabPanelsClassName?: string;
|
||||||
|
}
|
||||||
|
export const Tab = ({ items, className, tabPanelClassName, tabPanelsClassName, ...restProps }: TabProps) => {
|
||||||
|
return (
|
||||||
|
<Tabs className={twMerge('flex flex-col gap-4', className)} {...restProps}>
|
||||||
|
<TabList className="flex items-center text-lg font-medium cursor-pointer" items={items}>
|
||||||
|
{(item) => (
|
||||||
|
<AriaTab
|
||||||
|
id={item.key}
|
||||||
|
className="relative px-7.5 text-dabeeo-black-47 data-[selected=true]:text-primary first:pl-1.5 data-[selected=true]:font-bold after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:h-[18px] after:w-px after:bg-gray-300 last:after:hidden outline-none data-focus-visible:ring-2 data-focus-visible:ring-offset-2 data-focus-visible:ring-primary"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</AriaTab>
|
||||||
|
)}
|
||||||
|
</TabList>
|
||||||
|
<TabPanels className={tabPanelsClassName} items={items}>
|
||||||
|
{(item) => (
|
||||||
|
<TabPanel id={item.key} className={tabPanelClassName}>
|
||||||
|
{item.children}
|
||||||
|
</TabPanel>
|
||||||
|
)}
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
web-app/app/shared/components/textarea/TextArea.tsx
Normal file
21
web-app/app/shared/components/textarea/TextArea.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { TextArea as AriaTextArea, TextAreaProps as AriaTextAreaProps } from 'react-aria-components';
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
interface TextAreaProps extends Omit<AriaTextAreaProps, 'className'> {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextArea = (props: TextAreaProps) => {
|
||||||
|
const { className, ...restProps } = props;
|
||||||
|
return (
|
||||||
|
<AriaTextArea
|
||||||
|
{...restProps}
|
||||||
|
className={twMerge(
|
||||||
|
'block px-3 py-2.5 text-sm text-dabeeo-black-34 border border-dabeeo-gray-be data-[hovered=true]:border-dabeeo-black-34 data-[focused=true]:text-dabeeo-black-34 data-focused:border-dabeeo-black-34 outline-0 resize-none',
|
||||||
|
'read-only:bg-dabeeo-yellow-secondary read-only:data-[hovered=true]:border-dabeeo-gray-be read-only:data-[focused=true]:border-dabeeo-gray-be',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
web-app/app/shared/components/textarea/index.ts
Normal file
1
web-app/app/shared/components/textarea/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './TextArea';
|
||||||
44
web-app/app/shared/components/tooltip/Tooltip.tsx
Normal file
44
web-app/app/shared/components/tooltip/Tooltip.tsx
Normal file
@@ -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<AriaTooltipProps, 'children'> {
|
||||||
|
bgColor?: 'white' | 'tertiary';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({ children, ...props }: TooltipProps) {
|
||||||
|
const { className, bgColor = 'white', offset = 10, ...restProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaTooltip
|
||||||
|
offset={offset}
|
||||||
|
{...restProps}
|
||||||
|
className={composeRenderProps(className, (className, renderProps) =>
|
||||||
|
style({ ...renderProps, className, bgColor })
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<OverlayArrow>
|
||||||
|
<OverlayArrowIcon className="block group-data-[placement=bottom]/tooltip:rotate-180 group-data-[placement=left]/tooltip:-rotate-90 group-data-[placement=right]/tooltip:rotate-90" />
|
||||||
|
</OverlayArrow>
|
||||||
|
<span className="block max-h-[200px] overflow-y-auto whitespace-pre-wrap">{children}</span>
|
||||||
|
</AriaTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
web-app/app/shared/components/tooltip/index.ts
Normal file
1
web-app/app/shared/components/tooltip/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Tooltip';
|
||||||
Reference in New Issue
Block a user