@uniai-fe/uds-primitives 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -5
- package/dist/styles.css +220 -303
- package/package.json +4 -4
- package/src/components/badge/markup/Badge.tsx +10 -0
- package/src/components/badge/styles/index.scss +2 -2
- package/src/components/badge/types/index.ts +1 -1
- package/src/components/button/index.scss +3 -1
- package/src/components/button/index.tsx +9 -1
- package/src/components/button/markup/ButtonDefault.tsx +162 -0
- package/src/components/button/markup/ButtonRounded.tsx +48 -0
- package/src/components/button/markup/ButtonText.tsx +49 -0
- package/src/components/button/markup/index.ts +3 -0
- package/src/components/button/styles/{index.scss → button.scss} +202 -424
- package/src/components/button/styles/round-button.scss +56 -0
- package/src/components/button/styles/text-button.scss +96 -0
- package/src/components/button/types/index.ts +110 -35
- package/src/components/button/types/templates.ts +33 -0
- package/src/components/button/utils/index.ts +19 -19
- package/src/components/checkbox/markup/Checkbox.tsx +20 -2
- package/src/components/checkbox/types/checkbox.ts +16 -0
- package/src/components/chip/markup/Chip.tsx +8 -0
- package/src/components/dialog/markup/{confirm-dialog.tsx → ConfirmDialog.tsx} +23 -0
- package/src/components/dialog/markup/{notice-dialog.tsx → NoticeDialog.tsx} +18 -0
- package/src/components/dialog/markup/index.tsx +2 -2
- package/src/components/dialog/types/index.ts +43 -0
- package/src/components/drawer/markup/{drawer.tsx → Drawer.tsx} +58 -0
- package/src/components/drawer/markup/index.tsx +1 -1
- package/src/components/drawer/types/index.ts +24 -0
- package/src/components/input/markup/text/Base.tsx +32 -3
- package/src/components/input/markup/text/Identification.tsx +15 -2
- package/src/components/input/markup/text/Password.tsx +35 -2
- package/src/components/input/markup/text/Phone.tsx +38 -2
- package/src/components/input/markup/text/Search.tsx +30 -1
- package/src/components/input/styles/index.scss +6 -6
- package/src/components/input/types/index.ts +22 -1
- package/src/components/input/utils/index.ts +6 -0
- package/src/components/navigation/markup/mobile/BottomNavigation.tsx +11 -0
- package/src/components/navigation/types/index.ts +22 -0
- package/src/components/pagination/markup/Carousel.tsx +1 -0
- package/src/components/pagination/markup/Count.tsx +1 -0
- package/src/components/pagination/markup/Pagination.tsx +2 -0
- package/src/components/radio/markup/Radio.tsx +16 -2
- package/src/components/radio/markup/RadioCard.tsx +8 -0
- package/src/components/radio/markup/RadioCardGroup.tsx +8 -0
- package/src/components/radio/types/radio.ts +39 -0
- package/src/components/segmented-control/markup/SegmentedControl.tsx +12 -0
- package/src/components/segmented-control/types/index.ts +16 -0
- package/src/components/tab/markup/TabContent.tsx +5 -0
- package/src/components/tab/markup/TabList.tsx +19 -2
- package/src/components/tab/markup/TabRoot.tsx +50 -4
- package/src/components/tab/markup/TabTrigger.tsx +9 -1
- package/src/components/tab/styles/index.scss +28 -10
- package/src/components/tab/types/index.ts +10 -0
- package/src/components/tab/utils/tab-context.ts +8 -2
- package/src/components/button/markup/Button.tsx +0 -175
- package/src/components/button/markup/index.tsx +0 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Navigation 항목 key 타입.
|
|
5
|
+
*/
|
|
3
6
|
export type NavigationItemKey = string;
|
|
4
7
|
|
|
5
8
|
interface NavigationItemBase {
|
|
@@ -10,11 +13,19 @@ interface NavigationItemBase {
|
|
|
10
13
|
ariaLabel?: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* href로 라우팅되는 navigation item.
|
|
18
|
+
* @property {string} href 이동 경로.
|
|
19
|
+
*/
|
|
13
20
|
export type NavigationHrefItem = NavigationItemBase & {
|
|
14
21
|
href: string;
|
|
15
22
|
onSelect?: never;
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 클릭 시 onSelect를 실행하는 action item.
|
|
27
|
+
* @property {(key: NavigationItemKey) => void} onSelect 선택 콜백.
|
|
28
|
+
*/
|
|
18
29
|
export type NavigationActionItem = NavigationItemBase & {
|
|
19
30
|
onSelect: (key: NavigationItemKey) => void;
|
|
20
31
|
href?: never;
|
|
@@ -22,6 +33,14 @@ export type NavigationActionItem = NavigationItemBase & {
|
|
|
22
33
|
|
|
23
34
|
export type NavigationItem = NavigationHrefItem | NavigationActionItem;
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* BottomNavigation props. 모바일 하단 내비게이션에서 사용한다.
|
|
38
|
+
* @property {NavigationItem[]} items 렌더링할 항목 배열.
|
|
39
|
+
* @property {NavigationItemKey | null} activeKey 현재 활성 key.
|
|
40
|
+
* @property {(key: NavigationItemKey) => void} [onActiveChange] active 변경 콜백.
|
|
41
|
+
* @property {string} [ariaLabel] nav 라벨. 없으면 `aria-label` prop을 사용한다.
|
|
42
|
+
* @property {boolean} [fixed=false] true면 뷰포트 하단 고정 스타일.
|
|
43
|
+
*/
|
|
25
44
|
export interface BottomNavigationProps extends Omit<
|
|
26
45
|
ComponentPropsWithoutRef<"nav">,
|
|
27
46
|
"children"
|
|
@@ -37,6 +56,9 @@ export interface NavigationClassNameOptions {
|
|
|
37
56
|
className?: string;
|
|
38
57
|
}
|
|
39
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 내비게이션 sitemap 옵션. 팝업, auth, page 스타일 등 메타데이터.
|
|
61
|
+
*/
|
|
40
62
|
export interface NavigationSitemapOptions {
|
|
41
63
|
popup?: boolean;
|
|
42
64
|
auth?: string[];
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
* @param {number} [props.current=1] 현재 step index(1-indexed).
|
|
17
17
|
* @param {(page: number) => void} [props.onPageChange]
|
|
18
18
|
* 전달 시 dot를 클릭해 이동할 수 있고, 생략하면 모든 dot가 disabled 된다.
|
|
19
|
+
* @param {string} [props.className] `.pagination` root className.
|
|
19
20
|
*/
|
|
20
21
|
const PaginationCarousel = forwardRef<
|
|
21
22
|
HTMLUListElement,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
* @param {number} props.total 전체 step 수.
|
|
14
14
|
* @param {number} [props.current=1] 현재 step position.
|
|
15
15
|
* @param {"small" | "xsmall"} [props.size="small"] 높이/타이포 크기 옵션.
|
|
16
|
+
* @param {string} [props.className] `.pagination` root className.
|
|
16
17
|
*/
|
|
17
18
|
const PaginationCount = forwardRef<HTMLDivElement, PaginationCountProps>(
|
|
18
19
|
({ total, current = 1, size = "small", className, ...restProps }, ref) => {
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
* @param {number} [props.current=1] 현재 페이지. total 범위를 벗어나면 자동 조정된다.
|
|
17
17
|
* @param {(page: number) => void} [props.onPageChange]
|
|
18
18
|
* 페이지 버튼 클릭 핸들러. 전달되지 않으면 모든 버튼이 disabled 처리되어 cursor가 제거된다.
|
|
19
|
+
* @param {string} [props.className] `.pagination` root className.
|
|
20
|
+
* @param {React.AriaAttributes} [props.aria-*] ul element에 전달될 기타 속성.
|
|
19
21
|
* @example
|
|
20
22
|
* ```tsx
|
|
21
23
|
* <Pagination total={10} current={3} onPageChange={setPage} />
|
|
@@ -17,7 +17,12 @@ const RADIO_HELPER_CLASSNAME = "radio-helper";
|
|
|
17
17
|
* Radio component; Radix RadioGroup item thin wrapper
|
|
18
18
|
* @component
|
|
19
19
|
* @param {RadioProps} props
|
|
20
|
-
* @param {"medium" | "large"} [props.size]
|
|
20
|
+
* @param {"medium" | "large"} [props.size="medium"] indicator/spacing 크기.
|
|
21
|
+
* @param {string} [props.className] root className.
|
|
22
|
+
* @param {boolean} [props.disabled] disabled 상태.
|
|
23
|
+
* @param {string} [props.id] 외부 id.
|
|
24
|
+
* @param {string} [props.value] option value.
|
|
25
|
+
* @param {(value: string) => void} [props.onClick] native click handler.
|
|
21
26
|
* @example
|
|
22
27
|
* <Radio value="option" />
|
|
23
28
|
*/
|
|
@@ -48,7 +53,16 @@ export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
|
|
|
48
53
|
* RadioField component; label/helper wrapper for single radioItem
|
|
49
54
|
* @component
|
|
50
55
|
* @param {RadioFieldProps} props
|
|
51
|
-
* @param {"medium" | "large"} [props.size]
|
|
56
|
+
* @param {"medium" | "large"} [props.size="medium"] indicator/spacing 크기.
|
|
57
|
+
* @param {React.ReactNode} [props.label] `<label>` 텍스트/노드.
|
|
58
|
+
* @param {React.ReactNode} [props.helperText] helper 텍스트.
|
|
59
|
+
* @param {ComponentPropsWithoutRef<"label">} [props.labelProps] label attr.
|
|
60
|
+
* @param {ComponentPropsWithoutRef<"p">} [props.helperTextProps] helper attr.
|
|
61
|
+
* @param {string} [props.fieldClassName] `.radio-field` className.
|
|
62
|
+
* @param {string} [props.labelWrapperClassName] 라벨 wrapper className.
|
|
63
|
+
* @param {boolean} [props.disabled] disabled 상태.
|
|
64
|
+
* @param {string} [props.value] option value.
|
|
65
|
+
* @param {(value: string) => void} [props.onClick] native click handler.
|
|
52
66
|
* @example
|
|
53
67
|
* <RadioField label="옵션" helperText="필수" value="option" />
|
|
54
68
|
*/
|
|
@@ -19,6 +19,14 @@ const RADIO_CARD_INDICATOR_CLASSNAME = "radio-card-indicator";
|
|
|
19
19
|
* RadioCard component; entire card acts as radio trigger
|
|
20
20
|
* @component
|
|
21
21
|
* @param {RadioCardProps} props
|
|
22
|
+
* @param {React.ReactNode} props.title 카드 타이틀.
|
|
23
|
+
* @param {React.ReactNode} [props.description] 상세 설명.
|
|
24
|
+
* @param {React.ReactNode} [props.badge] 오른쪽 상단 배지 슬롯.
|
|
25
|
+
* @param {"medium" | "large"} [props.size="medium"] 카드 높이/spacing.
|
|
26
|
+
* @param {boolean} [props.disabled] disabled 상태.
|
|
27
|
+
* @param {string} [props.className] root className.
|
|
28
|
+
* @param {string} [props.value] option value.
|
|
29
|
+
* @param {(value: string) => void} [props.onClick] native click handler.
|
|
22
30
|
* @example
|
|
23
31
|
* <RadioCard title="행복1농장" value="farm-1" />
|
|
24
32
|
*/
|
|
@@ -11,6 +11,14 @@ const RADIO_CARD_GROUP_CLASSNAME = "radio-card-group";
|
|
|
11
11
|
* RadioCardGroup component; renders RadioCard list with internal Radix Root
|
|
12
12
|
* @component
|
|
13
13
|
* @param {RadioCardGroupProps} props
|
|
14
|
+
* @param {RadioCardGroupOption[]} props.options 렌더링할 카드 옵션 배열.
|
|
15
|
+
* @param {"medium" | "large"} [props.size="medium"] 모든 카드 기본 size.
|
|
16
|
+
* @param {string} [props.highlightedValue] 강조 스타일을 적용할 option id.
|
|
17
|
+
* @param {RadioCardBadgeRender} [props.renderBadge] 옵션/상태 기반 badge 렌더 함수.
|
|
18
|
+
* @param {string} [props.className] root className.
|
|
19
|
+
* @param {string} [props.value] 제어형 value.
|
|
20
|
+
* @param {string} [props.defaultValue] 비제어 초기 value.
|
|
21
|
+
* @param {(value: string) => void} [props.onValueChange] value 변경 콜백.
|
|
14
22
|
* @example
|
|
15
23
|
* <RadioCardGroup options={[{ id: "farm-1", title: "농장" }]} value="farm-1" />
|
|
16
24
|
*/
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import type * as RadixRadioGroup from "@radix-ui/react-radio-group";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 지원하는 radio size.
|
|
6
|
+
*/
|
|
4
7
|
export type RadioSize = "medium" | "large";
|
|
5
8
|
|
|
6
9
|
export type RadioPrimitiveProps = ComponentPropsWithoutRef<
|
|
7
10
|
typeof RadixRadioGroup.Item
|
|
8
11
|
>;
|
|
9
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Radix Radio item에 size/className/id 확장을 더한 props.
|
|
15
|
+
* @property {"medium" | "large"} [size="medium"] indicator/spacing 크기.
|
|
16
|
+
* @property {string} [className] root className.
|
|
17
|
+
* @property {boolean} [disabled] disabled 상태.
|
|
18
|
+
* @property {string} [id] 외부 id.
|
|
19
|
+
*/
|
|
10
20
|
export interface RadioProps extends RadioPrimitiveProps {
|
|
11
21
|
size?: RadioSize;
|
|
12
22
|
className?: string;
|
|
@@ -14,6 +24,15 @@ export interface RadioProps extends RadioPrimitiveProps {
|
|
|
14
24
|
id?: string;
|
|
15
25
|
}
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* label/helperText wrapper를 포함한 RadioField props.
|
|
29
|
+
* @property {React.ReactNode} [label] `<label>` 텍스트/노드.
|
|
30
|
+
* @property {React.ReactNode} [helperText] helper 텍스트.
|
|
31
|
+
* @property {ComponentPropsWithoutRef<"p">} [helperTextProps] helper attr.
|
|
32
|
+
* @property {ComponentPropsWithoutRef<"label">} [labelProps] label attr.
|
|
33
|
+
* @property {string} [fieldClassName] `.radio-field` className.
|
|
34
|
+
* @property {string} [labelWrapperClassName] 라벨 wrapper className.
|
|
35
|
+
*/
|
|
17
36
|
export interface RadioFieldProps extends RadioProps {
|
|
18
37
|
label?: ReactNode;
|
|
19
38
|
helperText?: ReactNode;
|
|
@@ -31,11 +50,24 @@ interface RadioCardDisplayProps {
|
|
|
31
50
|
badge?: ReactNode;
|
|
32
51
|
}
|
|
33
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 카드 형태 Radio item props. title/description/badge를 포함한다.
|
|
55
|
+
*/
|
|
34
56
|
export interface RadioCardProps
|
|
35
57
|
extends
|
|
36
58
|
Omit<RadioProps, keyof RadioCardDisplayProps>,
|
|
37
59
|
RadioCardDisplayProps {}
|
|
38
60
|
|
|
61
|
+
/**
|
|
62
|
+
* RadioCardGroup 옵션 정의.
|
|
63
|
+
* @property {string} id 옵션 value/id.
|
|
64
|
+
* @property {React.ReactNode} title 카드 상단 텍스트.
|
|
65
|
+
* @property {React.ReactNode} [description] 설명 텍스트.
|
|
66
|
+
* @property {React.ReactNode} [badge] 오른쪽 배지 슬롯.
|
|
67
|
+
* @property {"medium" | "large"} [size] 개별 size override.
|
|
68
|
+
* @property {boolean} [disabled] 옵션 disabled.
|
|
69
|
+
* @property {string} [className] 카드 className.
|
|
70
|
+
*/
|
|
39
71
|
export interface RadioCardGroupOption extends RadioCardDisplayProps {
|
|
40
72
|
id: string;
|
|
41
73
|
size?: RadioSize;
|
|
@@ -53,6 +85,13 @@ export type RadioCardBadgeRender = (
|
|
|
53
85
|
context: RadioCardBadgeRenderContext,
|
|
54
86
|
) => ReactNode;
|
|
55
87
|
|
|
88
|
+
/**
|
|
89
|
+
* RadioCardGroup props. Radix RadioGroup Root props + 옵션 리스트/배지 렌더러를 받는다.
|
|
90
|
+
* @property {RadioCardGroupOption[]} options 렌더링할 카드 옵션 배열.
|
|
91
|
+
* @property {"medium" | "large"} [size="medium"] 전체 size 기본값.
|
|
92
|
+
* @property {string} [highlightedValue] 강조(스타일)할 option id.
|
|
93
|
+
* @property {RadioCardBadgeRender} [renderBadge] 옵션/상태 기반 badge 렌더 함수.
|
|
94
|
+
*/
|
|
56
95
|
export interface RadioCardGroupProps extends ComponentPropsWithoutRef<
|
|
57
96
|
typeof RadixRadioGroup.Root
|
|
58
97
|
> {
|
|
@@ -13,6 +13,18 @@ import type { SegmentedControlProps, SegmentedControlValue } from "../types";
|
|
|
13
13
|
const toNullableValue = (value?: SegmentedControlValue) =>
|
|
14
14
|
value === undefined || value === "" ? undefined : value;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* SegmentedControl — Radix SegmentedControl wrapper with keepSelected toggle.
|
|
18
|
+
* @component
|
|
19
|
+
* @param {SegmentedControlProps} props
|
|
20
|
+
* @param {SegmentedControlOption[]} props.options 렌더링할 옵션 배열.
|
|
21
|
+
* @param {string} props.ariaLabel 스크린리더용 라벨(필수).
|
|
22
|
+
* @param {boolean} [props.keepSelected=true] 선택된 항목을 다시 클릭했을 때 해제하지 않고 유지할지 여부.
|
|
23
|
+
* @param {SegmentedControlValue} [props.value] 제어형 value.
|
|
24
|
+
* @param {SegmentedControlValue} [props.defaultValue] 비제어 초기 value.
|
|
25
|
+
* @param {(value: SegmentedControlValue) => void} [props.onValueChange] 값 변경 콜백.
|
|
26
|
+
* @param {string} [props.className] root className.
|
|
27
|
+
*/
|
|
16
28
|
const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
17
29
|
(
|
|
18
30
|
{
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef } from "react";
|
|
2
2
|
import type { SegmentedControl as RadixSegmentedControlNamespace } from "@radix-ui/themes";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* SegmentedControl value 타입. 문자열 value를 사용한다.
|
|
6
|
+
*/
|
|
4
7
|
export type SegmentedControlValue = string;
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* SegmentedControl 옵션 정의.
|
|
11
|
+
* @property {SegmentedControlValue} value 고유 value.
|
|
12
|
+
* @property {string} label 표시 텍스트.
|
|
13
|
+
* @property {boolean} [disabled] 비활성화 여부.
|
|
14
|
+
*/
|
|
6
15
|
export interface SegmentedControlOption {
|
|
7
16
|
value: SegmentedControlValue;
|
|
8
17
|
label: string;
|
|
@@ -12,6 +21,13 @@ export interface SegmentedControlOption {
|
|
|
12
21
|
type RadixSegmentedControlRootProps = ComponentPropsWithoutRef<
|
|
13
22
|
typeof RadixSegmentedControlNamespace.Root
|
|
14
23
|
>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SegmentedControl props. Radix Root props에서 children을 제거하고 options/ariaLabel을 강제한다.
|
|
27
|
+
* @property {SegmentedControlOption[]} options 렌더할 옵션 배열.
|
|
28
|
+
* @property {string} ariaLabel 스크린리더용 라벨. 필수.
|
|
29
|
+
* @property {boolean} [keepSelected=true] 선택된 항목을 다시 클릭했을 때 해제하지 않고 유지할지 여부.
|
|
30
|
+
*/
|
|
15
31
|
export interface SegmentedControlProps extends Omit<
|
|
16
32
|
RadixSegmentedControlRootProps,
|
|
17
33
|
"children"
|
|
@@ -6,6 +6,11 @@ import { useTabContext } from "../utils";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* TabContent: trigger와 동일한 data attr을 전달해 스타일 일관성을 유지한다.
|
|
9
|
+
* @component
|
|
10
|
+
* @param {TabContentProps} props
|
|
11
|
+
* @param {string} props.value 매칭되는 trigger value.
|
|
12
|
+
* @param {string} [props.className] content className.
|
|
13
|
+
* @param {React.ReactNode} [props.children] 컨텐츠 영역.
|
|
9
14
|
*/
|
|
10
15
|
const TabContent = forwardRef<HTMLDivElement, TabContentProps>(
|
|
11
16
|
({ className, children, ...restProps }, forwardedRef) => {
|
|
@@ -7,11 +7,21 @@ import { useTabContext, DEFAULT_TAB_CONTEXT_VALUE } from "../utils";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* TabList: Tab trigger container with variant/tone propagation.
|
|
10
|
+
* @component
|
|
11
|
+
* @param {TabListProps} props
|
|
12
|
+
* @param {"line" | "fill"} [props.variant] 루트 override variant.
|
|
13
|
+
* @param {"small" | "medium" | "large"} [props.scale] 루트 override scale.
|
|
14
|
+
* @param {string} [props.color] 활성 색상 override.
|
|
15
|
+
* @param {boolean} [props.fullWidth=false] trigger들을 flex:1로 확장할지 여부.
|
|
16
|
+
* @param {string} [props.className] 리스트 className.
|
|
17
|
+
* @param {React.ReactNode} [props.children] TabTrigger 모음.
|
|
18
|
+
* @param {React.CSSProperties} [props.style] 추가 style.
|
|
10
19
|
*/
|
|
11
20
|
const TabList = forwardRef<HTMLDivElement, TabListProps>(
|
|
12
21
|
(
|
|
13
22
|
{
|
|
14
23
|
variant,
|
|
24
|
+
scale,
|
|
15
25
|
color,
|
|
16
26
|
fullWidth = false,
|
|
17
27
|
className,
|
|
@@ -24,12 +34,18 @@ const TabList = forwardRef<HTMLDivElement, TabListProps>(
|
|
|
24
34
|
const parentContext = useTabContext();
|
|
25
35
|
const resolvedVariant =
|
|
26
36
|
variant ?? parentContext.variant ?? DEFAULT_TAB_CONTEXT_VALUE.variant;
|
|
37
|
+
const resolvedScale =
|
|
38
|
+
scale ?? parentContext.scale ?? DEFAULT_TAB_CONTEXT_VALUE.scale;
|
|
27
39
|
const resolvedColor =
|
|
28
40
|
color ?? parentContext.color ?? DEFAULT_TAB_CONTEXT_VALUE.color;
|
|
29
41
|
|
|
30
42
|
useEffect(() => {
|
|
31
|
-
parentContext.
|
|
32
|
-
|
|
43
|
+
parentContext.setSharedConfig?.(
|
|
44
|
+
resolvedVariant,
|
|
45
|
+
resolvedColor,
|
|
46
|
+
resolvedScale,
|
|
47
|
+
);
|
|
48
|
+
}, [parentContext, resolvedVariant, resolvedColor, resolvedScale]);
|
|
33
49
|
|
|
34
50
|
const listStyle = useMemo<CSSProperties>(
|
|
35
51
|
() => ({
|
|
@@ -45,6 +61,7 @@ const TabList = forwardRef<HTMLDivElement, TabListProps>(
|
|
|
45
61
|
ref={forwardedRef}
|
|
46
62
|
className={clsx("tab-list", className)}
|
|
47
63
|
data-variant={resolvedVariant}
|
|
64
|
+
data-scale={resolvedScale}
|
|
48
65
|
data-full-width={fullWidth ? "true" : undefined}
|
|
49
66
|
data-color={resolvedColor}
|
|
50
67
|
style={listStyle}
|
|
@@ -2,16 +2,49 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
|
2
2
|
import clsx from "clsx";
|
|
3
3
|
import { forwardRef, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import type { CSSProperties } from "react";
|
|
5
|
-
import type { TabRootProps } from "../types";
|
|
5
|
+
import type { TabRootProps, TabScale } from "../types";
|
|
6
6
|
import { TabContext, DEFAULT_TAB_CONTEXT_VALUE } from "../utils";
|
|
7
7
|
|
|
8
|
+
const TAB_SCALE_STYLES: Record<TabScale, Record<string, string>> = {
|
|
9
|
+
small: {
|
|
10
|
+
"--tab-label-font-size": "var(--font-heading-xxsmall-size, 15px)",
|
|
11
|
+
"--tab-label-font-weight": "var(--font-heading-xxsmall-weight, 600)",
|
|
12
|
+
"--tab-height": "40px",
|
|
13
|
+
"--tab-padding-x": "var(--spacing-padding-4, 8px)",
|
|
14
|
+
},
|
|
15
|
+
medium: {
|
|
16
|
+
"--tab-label-font-size": "var(--font-heading-xsmall-size, 17px)",
|
|
17
|
+
"--tab-label-font-weight": "var(--font-heading-xsmall-weight, 600)",
|
|
18
|
+
"--tab-height": "48px",
|
|
19
|
+
"--tab-padding-x": "var(--spacing-padding-8, 24px)",
|
|
20
|
+
},
|
|
21
|
+
large: {
|
|
22
|
+
"--tab-label-font-size": "var(--font-heading-small-size, 19px)",
|
|
23
|
+
"--tab-label-font-weight": "var(--font-heading-small-weight, 600)",
|
|
24
|
+
"--tab-height": "56px",
|
|
25
|
+
"--tab-padding-x": "var(--spacing-padding-8, 24px)",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
8
29
|
/**
|
|
9
30
|
* TabRoot: TabsPrimitive.Root thin wrapper with tab data context.
|
|
31
|
+
* @component
|
|
32
|
+
* @param {TabRootProps} props
|
|
33
|
+
* @param {"line" | "fill"} [props.variant="line"] 스타일 토글.
|
|
34
|
+
* @param {"small" | "medium" | "large"} [props.scale="medium"] 높이/타이포 스케일.
|
|
35
|
+
* @param {string} [props.color] 활성 indicator 색상(CSS 변수 포함).
|
|
36
|
+
* @param {string} [props.className] 루트 className.
|
|
37
|
+
* @param {React.ReactNode} [props.children] TabList/Trigger/Content 구조.
|
|
38
|
+
* @param {React.CSSProperties} [props.style] 추가 style.
|
|
39
|
+
* @param {string} [props.value] 제어형 active value.
|
|
40
|
+
* @param {string} [props.defaultValue] 비제어 초기 value.
|
|
41
|
+
* @param {(value: string) => void} [props.onValueChange] 탭 변경 콜백.
|
|
10
42
|
*/
|
|
11
43
|
const TabRoot = forwardRef<HTMLDivElement, TabRootProps>(
|
|
12
44
|
(
|
|
13
45
|
{
|
|
14
46
|
variant: variantProp = DEFAULT_TAB_CONTEXT_VALUE.variant,
|
|
47
|
+
scale: scaleProp = DEFAULT_TAB_CONTEXT_VALUE.scale,
|
|
15
48
|
color: colorProp = DEFAULT_TAB_CONTEXT_VALUE.color,
|
|
16
49
|
className,
|
|
17
50
|
children,
|
|
@@ -22,6 +55,7 @@ const TabRoot = forwardRef<HTMLDivElement, TabRootProps>(
|
|
|
22
55
|
) => {
|
|
23
56
|
const [variant, setVariant] = useState(variantProp);
|
|
24
57
|
const [color, setColor] = useState(colorProp);
|
|
58
|
+
const [scale, setScale] = useState(scaleProp);
|
|
25
59
|
|
|
26
60
|
useEffect(() => {
|
|
27
61
|
setVariant(variantProp);
|
|
@@ -31,26 +65,37 @@ const TabRoot = forwardRef<HTMLDivElement, TabRootProps>(
|
|
|
31
65
|
setColor(colorProp);
|
|
32
66
|
}, [colorProp]);
|
|
33
67
|
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setScale(scaleProp);
|
|
70
|
+
}, [scaleProp]);
|
|
71
|
+
|
|
34
72
|
const contextValue = useMemo(
|
|
35
73
|
() => ({
|
|
36
74
|
variant,
|
|
37
75
|
color,
|
|
38
|
-
|
|
76
|
+
scale,
|
|
77
|
+
setSharedConfig: (
|
|
39
78
|
nextVariant: typeof variant,
|
|
40
79
|
nextColor: typeof color,
|
|
80
|
+
nextScale: typeof scale,
|
|
41
81
|
) => {
|
|
42
82
|
setVariant(nextVariant);
|
|
43
83
|
setColor(nextColor);
|
|
84
|
+
setScale(nextScale);
|
|
44
85
|
},
|
|
45
86
|
}),
|
|
46
|
-
[variant, color],
|
|
87
|
+
[variant, color, scale],
|
|
47
88
|
);
|
|
89
|
+
|
|
90
|
+
const resolvedScaleStyle =
|
|
91
|
+
TAB_SCALE_STYLES[scale] ?? TAB_SCALE_STYLES.medium;
|
|
48
92
|
const rootStyle = useMemo<CSSProperties>(
|
|
49
93
|
() => ({
|
|
50
94
|
...(style ?? {}),
|
|
51
95
|
"--tab-color-active": color,
|
|
96
|
+
...resolvedScaleStyle,
|
|
52
97
|
}),
|
|
53
|
-
[style, color],
|
|
98
|
+
[style, color, resolvedScaleStyle],
|
|
54
99
|
);
|
|
55
100
|
|
|
56
101
|
return (
|
|
@@ -60,6 +105,7 @@ const TabRoot = forwardRef<HTMLDivElement, TabRootProps>(
|
|
|
60
105
|
ref={forwardedRef}
|
|
61
106
|
className={clsx("tab-root", className)}
|
|
62
107
|
data-variant={variant}
|
|
108
|
+
data-scale={scale}
|
|
63
109
|
style={rootStyle}
|
|
64
110
|
>
|
|
65
111
|
{children}
|
|
@@ -19,10 +19,17 @@ const wrapLabel = (children: ReactNode) => {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* TabTrigger: 개별 탭 버튼. icon slot을 지원한다.
|
|
22
|
+
* @component
|
|
23
|
+
* @param {TabTriggerProps} props
|
|
24
|
+
* @param {React.ReactNode} [props.icon] 라벨 앞 슬롯.
|
|
25
|
+
* @param {React.ReactNode} [props.children] 라벨 콘텐츠.
|
|
26
|
+
* @param {string} [props.className] trigger className.
|
|
27
|
+
* @param {string} props.value 탭 value.
|
|
28
|
+
* @param {boolean} [props.disabled] disabled 상태.
|
|
22
29
|
*/
|
|
23
30
|
const TabTrigger = forwardRef<HTMLButtonElement, TabTriggerProps>(
|
|
24
31
|
({ icon, children, className, ...restProps }, forwardedRef) => {
|
|
25
|
-
const { variant } = useTabContext();
|
|
32
|
+
const { variant, scale } = useTabContext();
|
|
26
33
|
const hasIcon = Boolean(icon);
|
|
27
34
|
const normalizedChildren = wrapLabel(children);
|
|
28
35
|
|
|
@@ -32,6 +39,7 @@ const TabTrigger = forwardRef<HTMLButtonElement, TabTriggerProps>(
|
|
|
32
39
|
ref={forwardedRef}
|
|
33
40
|
className={clsx("tab-trigger", className)}
|
|
34
41
|
data-variant={variant}
|
|
42
|
+
data-scale={scale}
|
|
35
43
|
data-has-icon={hasIcon ? "true" : undefined}
|
|
36
44
|
data-disabled={restProps.disabled ? "true" : undefined}
|
|
37
45
|
>
|
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
/* Figma node 694:4619 측정값을 CSS 변수로 고정해 Storybook과 실 서비스 간 시각 편차를 줄인다. */
|
|
5
5
|
--tab-label-font-size: var(--font-heading-xsmall-size, 17px);
|
|
6
6
|
--tab-label-font-weight: var(--font-heading-xsmall-weight, 600);
|
|
7
|
-
--tab-label-line-height:
|
|
8
|
-
--tab-label-letter-spacing:
|
|
9
|
-
--tab-gap:
|
|
7
|
+
--tab-label-line-height: 1.4;
|
|
8
|
+
--tab-label-letter-spacing: 0px;
|
|
9
|
+
--tab-gap: var(--spacing-gap-2, 8px);
|
|
10
10
|
--tab-padding-y: 10px;
|
|
11
|
-
--tab-padding-x:
|
|
11
|
+
--tab-padding-x: var(--spacing-padding-8, 24px);
|
|
12
12
|
--tab-icon-gap: 6px;
|
|
13
|
-
--tab-line-track-color: var(
|
|
14
|
-
|
|
15
|
-
var(--color-cool-gray-85)
|
|
16
|
-
);
|
|
17
|
-
--tab-line-track-height: 1.6px;
|
|
13
|
+
--tab-line-track-color: var(--color-border-divider, #f2f2f3);
|
|
14
|
+
--tab-line-track-height: 1px;
|
|
18
15
|
--tab-line-indicator-height: 2px;
|
|
19
16
|
--tab-color-active-default: #1a6aff;
|
|
20
17
|
--tab-color-active: var(--tab-color-active-default);
|
|
@@ -24,12 +21,33 @@
|
|
|
24
21
|
--tab-fill-active-color: var(--color-common-100, #ffffff);
|
|
25
22
|
--tab-inactive-color: var(--color-label-alternative, #afb1b6);
|
|
26
23
|
--tab-disabled-opacity: 0.4;
|
|
24
|
+
--tab-height: 48px;
|
|
27
25
|
width: 100%;
|
|
28
26
|
display: flex;
|
|
29
27
|
flex-direction: column;
|
|
30
28
|
gap: var(--spacing-gap-3);
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
.tab-root:where([data-scale="small"]) {
|
|
32
|
+
--tab-label-font-size: var(--font-heading-xxsmall-size, 15px);
|
|
33
|
+
--tab-label-font-weight: var(--font-heading-xxsmall-weight, 600);
|
|
34
|
+
--tab-height: 40px;
|
|
35
|
+
--tab-padding-x: var(--spacing-padding-4, 8px);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.tab-root:where([data-scale="medium"]) {
|
|
39
|
+
--tab-label-font-size: var(--font-heading-xsmall-size, 17px);
|
|
40
|
+
--tab-label-font-weight: var(--font-heading-xsmall-weight, 600);
|
|
41
|
+
--tab-height: 48px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tab-root:where([data-scale="large"]) {
|
|
45
|
+
--tab-label-font-size: var(--font-heading-small-size, 19px);
|
|
46
|
+
--tab-label-font-weight: var(--font-heading-small-weight, 600);
|
|
47
|
+
--tab-height: 56px;
|
|
48
|
+
--tab-padding-x: var(--spacing-padding-8, 24px);
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
.tab-list {
|
|
34
52
|
display: flex;
|
|
35
53
|
align-items: stretch;
|
|
@@ -53,6 +71,7 @@
|
|
|
53
71
|
align-items: center;
|
|
54
72
|
justify-content: center;
|
|
55
73
|
gap: var(--tab-icon-gap);
|
|
74
|
+
min-height: var(--tab-height);
|
|
56
75
|
padding: var(--tab-padding-y) var(--tab-padding-x);
|
|
57
76
|
background: transparent;
|
|
58
77
|
border: none;
|
|
@@ -158,7 +177,6 @@
|
|
|
158
177
|
.tab-trigger:where([data-variant="fill"]) {
|
|
159
178
|
border-radius: 12px;
|
|
160
179
|
min-width: 0;
|
|
161
|
-
padding: var(--tab-padding-y) var(--tab-padding-x);
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
.tab-trigger:where([data-variant="fill"][data-state="active"]) {
|
|
@@ -7,14 +7,20 @@ import type {
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
|
|
9
9
|
export const TAB_VARIANTS = ["line", "fill"] as const;
|
|
10
|
+
export const TAB_SCALES = ["small", "medium", "large"] as const;
|
|
10
11
|
|
|
11
12
|
export type TabVariant = (typeof TAB_VARIANTS)[number];
|
|
13
|
+
export type TabScale = (typeof TAB_SCALES)[number];
|
|
12
14
|
|
|
13
15
|
export interface TabRootProps extends TabsProps {
|
|
14
16
|
/**
|
|
15
17
|
* line / fill 스타일 토글. 기본 line.
|
|
16
18
|
*/
|
|
17
19
|
variant?: TabVariant;
|
|
20
|
+
/**
|
|
21
|
+
* small / medium / large 스케일. 기본 medium.
|
|
22
|
+
*/
|
|
23
|
+
scale?: TabScale;
|
|
18
24
|
/**
|
|
19
25
|
* 활성 색상(hex, rgba, css 변수 등). 지정하지 않으면 기본 블루.
|
|
20
26
|
*/
|
|
@@ -26,6 +32,10 @@ export interface TabListProps extends TabsListProps {
|
|
|
26
32
|
* 루트와 동일한 variant를 재지정할 수 있다.
|
|
27
33
|
*/
|
|
28
34
|
variant?: TabVariant;
|
|
35
|
+
/**
|
|
36
|
+
* 루트와 동일한 scale을 재지정할 수 있다.
|
|
37
|
+
*/
|
|
38
|
+
scale?: TabScale;
|
|
29
39
|
/**
|
|
30
40
|
* 리스트 단위 활성 색상 지정.
|
|
31
41
|
*/
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type { TabVariant } from "../types";
|
|
2
|
+
import type { TabScale, TabVariant } from "../types";
|
|
3
3
|
|
|
4
4
|
interface TabContextValue {
|
|
5
5
|
variant: TabVariant;
|
|
6
6
|
color: string;
|
|
7
|
-
|
|
7
|
+
scale: TabScale;
|
|
8
|
+
setSharedConfig?: (
|
|
9
|
+
nextVariant: TabVariant,
|
|
10
|
+
nextColor: string,
|
|
11
|
+
nextScale: TabScale,
|
|
12
|
+
) => void;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
const DEFAULT_TAB_CONTEXT_VALUE: TabContextValue = {
|
|
11
16
|
variant: "line",
|
|
12
17
|
color: "var(--tab-color-active-default, #1a6aff)",
|
|
18
|
+
scale: "medium",
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
const TabContext = createContext<TabContextValue>(DEFAULT_TAB_CONTEXT_VALUE);
|