@uniai-fe/uds-primitives 0.2.11 → 0.3.1
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/dist/styles.css +1118 -890
- package/package.json +1 -1
- package/src/components/button/index.tsx +1 -1
- package/src/components/button/markup/Base.tsx +1 -1
- package/src/components/button/styles/button.scss +32 -0
- package/src/components/button/styles/variables.scss +16 -0
- package/src/components/button/types/index.ts +3 -179
- package/src/components/button/types/{constants.ts → options.ts} +28 -13
- package/src/components/button/types/props.ts +264 -0
- package/src/components/chip/markup/Chip.tsx +14 -2
- package/src/components/chip/styles/chip.scss +154 -0
- package/src/components/chip/styles/index.scss +2 -138
- package/src/components/chip/styles/variables.scss +26 -0
- package/src/components/chip/types/index.ts +2 -52
- package/src/components/chip/types/options.ts +38 -0
- package/src/components/chip/types/props.ts +115 -0
- package/src/components/chip/utils/class-name.ts +36 -0
- package/src/components/chip/utils/index.ts +1 -36
- package/src/components/form/index.tsx +3 -16
- package/src/components/form/markup/form-field/index.tsx +1 -0
- package/src/components/form/markup/index.tsx +11 -0
- package/src/components/form/styles/index.scss +2 -2
- package/src/components/form/utils/index.ts +1 -0
- package/src/components/input/markup/foundation/Button.tsx +1 -6
- package/src/components/input/markup/text/AuthCode.tsx +3 -1
- package/src/components/input/markup/text/Email.tsx +16 -11
- package/src/components/input/markup/text/Password.tsx +3 -3
- package/src/components/input/markup/text/Phone.tsx +30 -28
- package/src/components/input/markup/text/Search.tsx +3 -1
- package/src/components/input/types/text.ts +10 -5
- package/src/components/input/utils/index.tsx +0 -1
- package/src/components/table/markup/Container.tsx +36 -70
- package/src/components/table/markup/foundation/Cell.tsx +4 -6
- package/src/components/table/styles/foundation.scss +75 -9
- package/src/components/table/types/foundation.ts +62 -59
- package/src/index.scss +7 -6
- package/src/index.tsx +19 -22
- package/src/components/button/types/templates.ts +0 -84
- package/src/components/input/utils/verification.tsx +0 -35
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
@use "./form-field/variables.scss";
|
|
2
|
-
@use "./form-field/layout.scss";
|
|
1
|
+
@use "./form-field/variables.scss" as form-field-variables;
|
|
2
|
+
@use "./form-field/layout.scss" as form-field-layout;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./form-field";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "../../../button";
|
|
4
|
-
import type { ButtonProps } from "../../../button/types";
|
|
5
4
|
import type { InputUtilityButtonProps } from "../../types";
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -19,17 +18,13 @@ export default function InputBaseUtilityButton({
|
|
|
19
18
|
disabled,
|
|
20
19
|
onClick,
|
|
21
20
|
}: InputUtilityButtonProps) {
|
|
22
|
-
const handleButtonClick: ButtonProps["onClick"] = event => {
|
|
23
|
-
onClick?.(event);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
21
|
return (
|
|
27
22
|
<Button.Default
|
|
28
23
|
fill="outlined"
|
|
29
24
|
size="small"
|
|
30
25
|
priority={priority}
|
|
31
26
|
className="input-utility-button"
|
|
32
|
-
onClick={
|
|
27
|
+
onClick={event => onClick?.(event)}
|
|
33
28
|
disabled={disabled}
|
|
34
29
|
>
|
|
35
30
|
{children}
|
|
@@ -22,7 +22,7 @@ const DEFAULT_PLACEHOLDER = "인증코드 입력";
|
|
|
22
22
|
const DEFAULT_COUNTDOWN_ACTION_LABEL = "시간연장";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Input Preset; AuthCode 카운트다운 입력
|
|
26
26
|
* @component
|
|
27
27
|
* @param {AuthCodeInputProps} props 인증코드 입력 props
|
|
28
28
|
* @property {string} [props.value] 제어형 인증코드 값
|
|
@@ -36,6 +36,8 @@ const DEFAULT_COUNTDOWN_ACTION_LABEL = "시간연장";
|
|
|
36
36
|
* @property {ReactNode} [props.countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨
|
|
37
37
|
* @property {InputUtilityButtonClickHandler} [props.onCountdownAction] 연장 버튼 클릭 핸들러
|
|
38
38
|
* @property {boolean} [props.countdownActionDisabled] 연장 버튼 disabled 여부
|
|
39
|
+
* @example
|
|
40
|
+
* <AuthCodeInput length={6} onComplete={code => console.log(code)} />
|
|
39
41
|
*/
|
|
40
42
|
const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
|
|
41
43
|
(
|
|
@@ -5,10 +5,10 @@ import { forwardRef, useCallback } from "react";
|
|
|
5
5
|
|
|
6
6
|
import type { EmailInputProps } from "../../types";
|
|
7
7
|
import InputBase from "../foundation/Input";
|
|
8
|
-
import
|
|
8
|
+
import InputBaseUtilityButton from "../foundation/Button";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Input Preset; Email 인증요청 조합 입력
|
|
12
12
|
* @component
|
|
13
13
|
* @param {EmailInputProps} props 이메일 입력 props
|
|
14
14
|
* @property {string} [props.value] 제어형 이메일 값
|
|
@@ -19,6 +19,8 @@ import { renderVerificationRequestButton } from "../../utils/verification";
|
|
|
19
19
|
* @property {ReactNode} [props.requestButtonLabel] 버튼 라벨
|
|
20
20
|
* @property {boolean} [props.requestButtonDisabled] 버튼 disabled 상태
|
|
21
21
|
* @property {ReactNode} [props.right] 기본 오른쪽 슬롯 override
|
|
22
|
+
* @example
|
|
23
|
+
* <EmailInput onRequestCode={() => {}} />
|
|
22
24
|
*/
|
|
23
25
|
const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
24
26
|
(
|
|
@@ -35,7 +37,6 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
35
37
|
},
|
|
36
38
|
forwardedRef,
|
|
37
39
|
) => {
|
|
38
|
-
// 이메일 값 변경 시 상위 상태(onValueChange)와 native onChange를 모두 호출한다.
|
|
39
40
|
const handleChange = useCallback(
|
|
40
41
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
41
42
|
onValueChange?.(event.currentTarget.value);
|
|
@@ -44,13 +45,6 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
44
45
|
[onChange, onValueChange],
|
|
45
46
|
);
|
|
46
47
|
|
|
47
|
-
// 인증 요청 옵션이 있을 때만 우측 액션 버튼을 만든다.
|
|
48
|
-
const actionButton = renderVerificationRequestButton({
|
|
49
|
-
onRequestCode,
|
|
50
|
-
requestButtonDisabled,
|
|
51
|
-
requestButtonLabel,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
48
|
return (
|
|
55
49
|
<div className="email-verification">
|
|
56
50
|
<InputBase
|
|
@@ -61,7 +55,18 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
|
61
55
|
value={value}
|
|
62
56
|
defaultValue={defaultValue}
|
|
63
57
|
onChange={handleChange}
|
|
64
|
-
right={
|
|
58
|
+
right={
|
|
59
|
+
right ??
|
|
60
|
+
(onRequestCode ? (
|
|
61
|
+
<InputBaseUtilityButton
|
|
62
|
+
priority="primary"
|
|
63
|
+
onClick={onRequestCode}
|
|
64
|
+
disabled={requestButtonDisabled}
|
|
65
|
+
>
|
|
66
|
+
{requestButtonLabel}
|
|
67
|
+
</InputBaseUtilityButton>
|
|
68
|
+
) : null)
|
|
69
|
+
}
|
|
65
70
|
/>
|
|
66
71
|
</div>
|
|
67
72
|
);
|
|
@@ -7,13 +7,15 @@ import HideOffIcon from "../../img/hide-off.svg";
|
|
|
7
7
|
import HideOnIcon from "../../img/hide-on.svg";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Input Preset; Password 표시 전환 입력
|
|
11
11
|
* @component
|
|
12
12
|
* @param {InputPasswordProps} props
|
|
13
13
|
* @property {boolean} [props.defaultVisible=false] 초기 노출 여부
|
|
14
14
|
* @property {{show: string; hide: string}} [props.toggleLabel] 토글 버튼 라벨
|
|
15
15
|
* @property {ReactNode} [props.right] 오른쪽 슬롯 override
|
|
16
16
|
* @property {string | number | readonly string[]} [props.defaultValue] 초기값
|
|
17
|
+
* @example
|
|
18
|
+
* <PasswordInput defaultVisible={false} />
|
|
17
19
|
*/
|
|
18
20
|
const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
19
21
|
(
|
|
@@ -36,13 +38,11 @@ const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
|
|
|
36
38
|
<button
|
|
37
39
|
type="button"
|
|
38
40
|
className="input-password-toggle"
|
|
39
|
-
// Tab 이동 시 다음 입력 필드로 바로 넘어가도록 토글 버튼은 순서에서 제외한다.
|
|
40
41
|
tabIndex={-1}
|
|
41
42
|
onClick={handleToggle}
|
|
42
43
|
aria-pressed={visible}
|
|
43
44
|
aria-label={visible ? toggleLabel.hide : toggleLabel.show}
|
|
44
45
|
>
|
|
45
|
-
{/* 토글 상태에 따라 hide-on/off 아이콘을 교체한다. */}
|
|
46
46
|
{visible ? (
|
|
47
47
|
<HideOffIcon aria-hidden="true" />
|
|
48
48
|
) : (
|
|
@@ -5,8 +5,8 @@ import type { ChangeEvent } from "react";
|
|
|
5
5
|
import { forwardRef, useCallback, useMemo } from "react";
|
|
6
6
|
import type { PhoneInputProps } from "../../types";
|
|
7
7
|
import InputBase from "../foundation/Input";
|
|
8
|
-
import { renderVerificationRequestButton } from "../../utils/verification";
|
|
9
8
|
import { useDigitField } from "../../hooks/useDigitField";
|
|
9
|
+
import InputBaseUtilityButton from "../foundation/Button";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 휴대폰 입력에서 허용할 숫자 자리수. 국내 번호 11자 기준.
|
|
@@ -14,22 +14,20 @@ import { useDigitField } from "../../hooks/useDigitField";
|
|
|
14
14
|
const MAX_DIGITS = 11;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
const formatPhoneNumber = (digits: string) => maskPhone(digits);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 휴대폰 번호 입력 컴포넌트; 마스킹 처리와 인증요청 버튼까지만 담당한다.
|
|
17
|
+
* Input Preset; Phone 인증요청 조합 입력
|
|
23
18
|
* @component
|
|
24
19
|
* @param {PhoneInputProps} props 휴대폰 입력 props
|
|
25
|
-
* @
|
|
26
|
-
* @
|
|
27
|
-
* @
|
|
28
|
-
* @
|
|
29
|
-
* @
|
|
30
|
-
* @
|
|
31
|
-
* @
|
|
32
|
-
* @
|
|
20
|
+
* @property {string} [props.value] 제어형 포맷 값
|
|
21
|
+
* @property {string} [props.defaultValue] 비제어 초기값
|
|
22
|
+
* @property {(value: string, digits: string) => void} [props.onValueChange] 포맷/숫자 변경 콜백
|
|
23
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override
|
|
24
|
+
* @property {InputUtilityButtonClickHandler} [props.onRequestCode] 인증 요청 버튼 클릭 시 호출
|
|
25
|
+
* @property {ReactNode} [props.requestButtonLabel] 인증 요청 버튼 라벨
|
|
26
|
+
* @property {boolean} [props.requestButtonDisabled] 인증 요청 버튼 disabled
|
|
27
|
+
* @property {string} [props.placeholder] 전화번호 placeholder 텍스트
|
|
28
|
+
* @property {ReactNode} [props.right] 기본 오른쪽 슬롯 override
|
|
29
|
+
* @example
|
|
30
|
+
* <PhoneInput onRequestCode={() => {}} />
|
|
33
31
|
*/
|
|
34
32
|
const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
35
33
|
(
|
|
@@ -41,50 +39,54 @@ const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
41
39
|
onRequestCode,
|
|
42
40
|
requestButtonLabel = "인증번호 요청",
|
|
43
41
|
requestButtonDisabled,
|
|
42
|
+
// 기본 placeholder를 유지하되 호출자가 값을 전달하면 그대로 우선 적용한다.
|
|
43
|
+
placeholder = "000-0000-0000",
|
|
44
44
|
right,
|
|
45
45
|
...restProps
|
|
46
46
|
},
|
|
47
47
|
forwardedRef,
|
|
48
48
|
) => {
|
|
49
|
-
// 제어/비제어 숫자 입력을 통합 관리한다.
|
|
50
49
|
const { digits, handleDigitsChange } = useDigitField({
|
|
51
50
|
value,
|
|
52
51
|
defaultValue,
|
|
53
52
|
maxLength: MAX_DIGITS,
|
|
54
53
|
});
|
|
55
|
-
const formattedValue = useMemo(() =>
|
|
54
|
+
const formattedValue = useMemo(() => maskPhone(digits), [digits]);
|
|
56
55
|
|
|
57
56
|
const handleChange = useCallback(
|
|
58
57
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
59
58
|
const digitsOnly = handleDigitsChange(event);
|
|
60
|
-
const formatted =
|
|
59
|
+
const formatted = maskPhone(digitsOnly);
|
|
61
60
|
if (event.currentTarget.value !== formatted) {
|
|
62
61
|
event.currentTarget.value = formatted;
|
|
63
62
|
}
|
|
64
|
-
// 포맷팅된 값과 숫자만으로 분리된 값을 모두 호출자에게 전달한다.
|
|
65
63
|
onValueChange?.(formatted, digitsOnly);
|
|
66
64
|
onChange?.(event);
|
|
67
65
|
},
|
|
68
66
|
[handleDigitsChange, onChange, onValueChange],
|
|
69
67
|
);
|
|
70
68
|
|
|
71
|
-
const actionButton = renderVerificationRequestButton({
|
|
72
|
-
onRequestCode,
|
|
73
|
-
requestButtonDisabled,
|
|
74
|
-
requestButtonLabel,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// 기본 전화 입력 필드; 인증 UI 노출 여부에 따라 래핑된다.
|
|
78
69
|
const phoneField = (
|
|
79
70
|
<InputBase
|
|
80
71
|
{...restProps}
|
|
81
72
|
ref={forwardedRef}
|
|
82
73
|
type="tel"
|
|
83
74
|
inputMode="tel"
|
|
84
|
-
placeholder=
|
|
75
|
+
placeholder={placeholder}
|
|
85
76
|
value={formattedValue}
|
|
86
77
|
onChange={handleChange}
|
|
87
|
-
right={
|
|
78
|
+
right={
|
|
79
|
+
right ??
|
|
80
|
+
(onRequestCode ? (
|
|
81
|
+
<InputBaseUtilityButton
|
|
82
|
+
priority="primary"
|
|
83
|
+
onClick={onRequestCode}
|
|
84
|
+
disabled={requestButtonDisabled}
|
|
85
|
+
>
|
|
86
|
+
{requestButtonLabel}
|
|
87
|
+
</InputBaseUtilityButton>
|
|
88
|
+
) : null)
|
|
89
|
+
}
|
|
88
90
|
/>
|
|
89
91
|
);
|
|
90
92
|
|
|
@@ -4,11 +4,13 @@ import InputBase from "../foundation/Input";
|
|
|
4
4
|
import SearchIcon from "../../img/search.svg";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Input Preset; Search 아이콘 고정 입력
|
|
8
8
|
* @component
|
|
9
9
|
* @param {SearchInputProps} props
|
|
10
10
|
* @property {ReactNode} [props.left] left 슬롯 override
|
|
11
11
|
* @property {ReactNode} [props.right] right 슬롯 override
|
|
12
|
+
* @example
|
|
13
|
+
* <SearchInput placeholder="검색어 입력" />
|
|
12
14
|
*/
|
|
13
15
|
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
14
16
|
({ left, ...restProps }, forwardedRef) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChangeEvent, MouseEvent, ReactNode } from "react";
|
|
2
2
|
import type { ButtonPriority } from "../../button/types";
|
|
3
3
|
import type { InputProps } from "./foundation";
|
|
4
4
|
|
|
@@ -64,7 +64,7 @@ export interface InputPasswordProps extends Omit<InputProps, "type"> {
|
|
|
64
64
|
* @property {string} [value] 제어형 값
|
|
65
65
|
* @property {string} [defaultValue] 비제어 초기값
|
|
66
66
|
* @property {(value:string)=>void} [onValueChange] 값 변경 시 호출
|
|
67
|
-
* @property {
|
|
67
|
+
* @property {(event: ChangeEvent<HTMLInputElement>) => void} [onChange] native onChange override
|
|
68
68
|
* @property {number} [length] 자리수 제한
|
|
69
69
|
* @property {(code:string)=>void} [onComplete] 입력 완료 콜백
|
|
70
70
|
* @property {string} [placeholder] placeholder 텍스트
|
|
@@ -92,7 +92,7 @@ export interface AuthCodeInputProps extends Omit<
|
|
|
92
92
|
/**
|
|
93
93
|
* native onChange override
|
|
94
94
|
*/
|
|
95
|
-
onChange?:
|
|
95
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
96
96
|
/**
|
|
97
97
|
* 자리수 제한
|
|
98
98
|
*/
|
|
@@ -148,7 +148,7 @@ export interface EmailInputProps extends Omit<
|
|
|
148
148
|
/**
|
|
149
149
|
* native onChange override
|
|
150
150
|
*/
|
|
151
|
-
onChange?:
|
|
151
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
152
152
|
/**
|
|
153
153
|
* 인증 요청 클릭 핸들러
|
|
154
154
|
*/
|
|
@@ -166,6 +166,7 @@ export interface EmailInputProps extends Omit<
|
|
|
166
166
|
/**
|
|
167
167
|
* Phone Input props; 전화번호 포맷팅과 인증요청 버튼만 담당한다.
|
|
168
168
|
* @property {(value:string,digits:string)=>void} [onValueChange] 포맷/숫자 콜백
|
|
169
|
+
* @property {string} [placeholder] 전화번호 placeholder 텍스트
|
|
169
170
|
* 나머지 props는 InputProps 기반이며 type 관련 속성을 제외한다.
|
|
170
171
|
*/
|
|
171
172
|
export interface PhoneInputProps extends Omit<
|
|
@@ -187,7 +188,7 @@ export interface PhoneInputProps extends Omit<
|
|
|
187
188
|
/**
|
|
188
189
|
* native onChange override
|
|
189
190
|
*/
|
|
190
|
-
onChange?:
|
|
191
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
191
192
|
/**
|
|
192
193
|
* 인증 요청 핸들러
|
|
193
194
|
*/
|
|
@@ -200,4 +201,8 @@ export interface PhoneInputProps extends Omit<
|
|
|
200
201
|
* 인증 요청 disabled
|
|
201
202
|
*/
|
|
202
203
|
requestButtonDisabled?: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* 전화번호 placeholder 텍스트
|
|
206
|
+
*/
|
|
207
|
+
placeholder?: string;
|
|
203
208
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { forwardRef } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TableContainerProps } from "../types";
|
|
3
3
|
import TableBody from "./foundation/Body";
|
|
4
4
|
import TableCell from "./foundation/Cell";
|
|
5
5
|
import TableHead from "./foundation/Head";
|
|
@@ -7,33 +7,12 @@ import TableTh from "./foundation/Th";
|
|
|
7
7
|
import TableRoot from "./foundation/Root";
|
|
8
8
|
import TableRow from "./foundation/Row";
|
|
9
9
|
|
|
10
|
-
const resolveHeadContent = (column: TableTemplateColumn): React.ReactNode => {
|
|
11
|
-
if (typeof column.cellContents !== "undefined") {
|
|
12
|
-
return column.cellContents;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (typeof column.cellChildren !== "undefined") {
|
|
16
|
-
return column.cellChildren;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (typeof column.headContent !== "undefined") {
|
|
20
|
-
return column.headContent;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (typeof column.dataName !== "undefined") {
|
|
24
|
-
return column.dataName;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return column.dataKey;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
10
|
/**
|
|
31
11
|
* Table Preset; 기본 Container 조합 컴포넌트
|
|
32
12
|
* @component
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {
|
|
13
|
+
* @param {TableContainerProps} props
|
|
14
|
+
* @param {TableColumnData[]} [props.columns] colgroup/head 자동 렌더링 column 목록
|
|
35
15
|
* @param {boolean} [props.isCustomBody=false] true면 body wrapper 없이 children 직접 렌더링
|
|
36
|
-
* @param {"legacy-rem" | "raw"} [props.widthMode="legacy-rem"] number width 해석 방식
|
|
37
16
|
* @param {React.ReactNode} [props.footer] footer 노드. `<Table.Foot />`를 직접 전달하는 방식을 권장
|
|
38
17
|
* @param {React.ReactNode} [props.children] table body 콘텐츠
|
|
39
18
|
* @example
|
|
@@ -52,63 +31,50 @@ const resolveHeadContent = (column: TableTemplateColumn): React.ReactNode => {
|
|
|
52
31
|
* {rows}
|
|
53
32
|
* </Table.Container>
|
|
54
33
|
*/
|
|
55
|
-
const TableContainer = forwardRef<HTMLTableElement,
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
footer,
|
|
62
|
-
children,
|
|
63
|
-
...tableProps
|
|
64
|
-
},
|
|
65
|
-
ref,
|
|
66
|
-
) => {
|
|
67
|
-
const hasColumns = Array.isArray(columns) && columns.length > 0;
|
|
34
|
+
const TableContainer = forwardRef<HTMLTableElement, TableContainerProps>(
|
|
35
|
+
({ columns, isCustomBody = false, footer, children, ...tableProps }, ref) => {
|
|
36
|
+
// 변경: optional columns를 안전하게 순회하기 위한 기본 배열을 고정한다.
|
|
37
|
+
const resolvedColumns = columns ?? [];
|
|
38
|
+
// 변경: columns 존재 여부만 단일 플래그로 관리해 렌더 분기 가독성을 유지한다.
|
|
39
|
+
const hasColumns = resolvedColumns.length > 0;
|
|
68
40
|
|
|
69
41
|
return (
|
|
70
42
|
<TableRoot {...tableProps} ref={ref}>
|
|
71
43
|
{hasColumns && (
|
|
72
44
|
<colgroup>
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
? {
|
|
90
|
-
width: normalizedWidth,
|
|
91
|
-
}
|
|
92
|
-
: undefined
|
|
93
|
-
}
|
|
94
|
-
width={typeof width === "number" ? width : undefined}
|
|
95
|
-
/>
|
|
96
|
-
);
|
|
97
|
-
})}
|
|
45
|
+
{resolvedColumns.map(({ key, width, dataKey, className }) => (
|
|
46
|
+
<col
|
|
47
|
+
key={`${key}/colgroup`}
|
|
48
|
+
className={className}
|
|
49
|
+
data-key={dataKey}
|
|
50
|
+
style={
|
|
51
|
+
typeof width !== "undefined"
|
|
52
|
+
? {
|
|
53
|
+
// 변경: width는 number|string 값을 그대로 사용하고, 단위 해석은 사용처에서 결정한다.
|
|
54
|
+
width,
|
|
55
|
+
}
|
|
56
|
+
: undefined
|
|
57
|
+
}
|
|
58
|
+
width={typeof width === "number" ? width : undefined}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
98
61
|
</colgroup>
|
|
99
62
|
)}
|
|
100
63
|
|
|
101
64
|
{hasColumns && (
|
|
102
65
|
<TableHead>
|
|
103
66
|
<TableRow>
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
67
|
+
{resolvedColumns.map(
|
|
68
|
+
({ key, dataKey, alignX, alignY, cellContents }) => (
|
|
69
|
+
<TableTh key={`${key}/head`} data-key={dataKey}>
|
|
70
|
+
{/* 변경: header cell 정렬은 alignX/alignY로만 제어한다. */}
|
|
71
|
+
<TableCell section="head" alignX={alignX} alignY={alignY}>
|
|
72
|
+
{/* 변경: key는 렌더 식별자이므로 헤더 노출값 fallback으로 사용하지 않는다. */}
|
|
73
|
+
{cellContents}
|
|
74
|
+
</TableCell>
|
|
75
|
+
</TableTh>
|
|
76
|
+
),
|
|
77
|
+
)}
|
|
112
78
|
</TableRow>
|
|
113
79
|
</TableHead>
|
|
114
80
|
)}
|
|
@@ -6,13 +6,12 @@ import type { TableCellContentProps } from "../../types";
|
|
|
6
6
|
* Table Foundation; Cell 콘텐츠 래퍼 컴포넌트
|
|
7
7
|
* @component
|
|
8
8
|
* @param {TableCellContentProps} props
|
|
9
|
-
* @param {"left" | "center" | "right"} [props.alignX] cell 콘텐츠 가로 정렬
|
|
10
|
-
* @param {"top" | "center" | "bottom"} [props.alignY] cell 콘텐츠 세로 정렬
|
|
9
|
+
* @param {"left" | "center" | "right" | "normal" | "start" | "end" | "flex-start" | "flex-end" | "space-between" | "space-around" | "space-evenly" | "stretch"} [props.alignX] cell 콘텐츠 가로 정렬(CSS `justify-content`로 매핑)
|
|
10
|
+
* @param {"top" | "center" | "bottom" | "normal" | "stretch" | "start" | "end" | "flex-start" | "flex-end" | "self-start" | "self-end" | "baseline"} [props.alignY] cell 콘텐츠 세로 정렬(CSS `align-items`로 매핑)
|
|
11
11
|
* @param {boolean} [props.noPadding=false] true면 cell 내부 padding 제거
|
|
12
12
|
* @param {"default" | "none"} [props.padding="default"] cell 내부 padding 제어
|
|
13
|
-
* @param {"left" | "center" | "right"} [props.align] cell 콘텐츠 가로 정렬(호환 alias)
|
|
14
13
|
* @param {string} [props.className] cell content className
|
|
15
|
-
* @param {React.ReactNode} [props.children] 셀 내부 콘텐츠
|
|
14
|
+
* @param {React.ReactNode} [props.children] 셀 내부 콘텐츠 `span.table-cell-text.table-${section}-cell-text`
|
|
16
15
|
*/
|
|
17
16
|
const TableCell = forwardRef<HTMLDivElement, TableCellContentProps>(
|
|
18
17
|
(
|
|
@@ -24,12 +23,11 @@ const TableCell = forwardRef<HTMLDivElement, TableCellContentProps>(
|
|
|
24
23
|
alignY,
|
|
25
24
|
noPadding = false,
|
|
26
25
|
padding = "default",
|
|
27
|
-
align,
|
|
28
26
|
...cellProps
|
|
29
27
|
},
|
|
30
28
|
ref,
|
|
31
29
|
) => {
|
|
32
|
-
const resolvedAlignX = alignX ??
|
|
30
|
+
const resolvedAlignX = alignX ?? "left";
|
|
33
31
|
const resolvedAlignY = alignY ?? "center";
|
|
34
32
|
// 변경: noPadding boolean이 주어지면 padding 옵션보다 우선해 none으로 고정한다.
|
|
35
33
|
const resolvedPadding = noPadding ? "none" : padding;
|
|
@@ -167,22 +167,52 @@
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// 변경: Cell.alignX/alignY로 콘텐츠 정렬을 제어한다.
|
|
170
|
-
.table-cell-content[data-align-x="left"]
|
|
171
|
-
.table-cell-content[data-align="left"] {
|
|
170
|
+
.table-cell-content[data-align-x="left"] {
|
|
172
171
|
justify-content: flex-start;
|
|
173
|
-
text-align: left;
|
|
174
172
|
}
|
|
175
173
|
|
|
176
|
-
.table-cell-content[data-align-x="center"]
|
|
177
|
-
.table-cell-content[data-align="center"] {
|
|
174
|
+
.table-cell-content[data-align-x="center"] {
|
|
178
175
|
justify-content: center;
|
|
179
|
-
text-align: center;
|
|
180
176
|
}
|
|
181
177
|
|
|
182
|
-
.table-cell-content[data-align-x="right"]
|
|
183
|
-
.table-cell-content[data-align="right"] {
|
|
178
|
+
.table-cell-content[data-align-x="right"] {
|
|
184
179
|
justify-content: flex-end;
|
|
185
|
-
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.table-cell-content[data-align-x="normal"] {
|
|
183
|
+
justify-content: normal;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.table-cell-content[data-align-x="start"] {
|
|
187
|
+
justify-content: start;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.table-cell-content[data-align-x="end"] {
|
|
191
|
+
justify-content: end;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.table-cell-content[data-align-x="flex-start"] {
|
|
195
|
+
justify-content: flex-start;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.table-cell-content[data-align-x="flex-end"] {
|
|
199
|
+
justify-content: flex-end;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.table-cell-content[data-align-x="space-between"] {
|
|
203
|
+
justify-content: space-between;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.table-cell-content[data-align-x="space-around"] {
|
|
207
|
+
justify-content: space-around;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.table-cell-content[data-align-x="space-evenly"] {
|
|
211
|
+
justify-content: space-evenly;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.table-cell-content[data-align-x="stretch"] {
|
|
215
|
+
justify-content: stretch;
|
|
186
216
|
}
|
|
187
217
|
|
|
188
218
|
.table-cell-content[data-align-y="top"] {
|
|
@@ -197,6 +227,42 @@
|
|
|
197
227
|
align-items: flex-end;
|
|
198
228
|
}
|
|
199
229
|
|
|
230
|
+
.table-cell-content[data-align-y="normal"] {
|
|
231
|
+
align-items: normal;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.table-cell-content[data-align-y="stretch"] {
|
|
235
|
+
align-items: stretch;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.table-cell-content[data-align-y="start"] {
|
|
239
|
+
align-items: start;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.table-cell-content[data-align-y="end"] {
|
|
243
|
+
align-items: end;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.table-cell-content[data-align-y="flex-start"] {
|
|
247
|
+
align-items: flex-start;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.table-cell-content[data-align-y="flex-end"] {
|
|
251
|
+
align-items: flex-end;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.table-cell-content[data-align-y="self-start"] {
|
|
255
|
+
align-items: self-start;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.table-cell-content[data-align-y="self-end"] {
|
|
259
|
+
align-items: self-end;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.table-cell-content[data-align-y="baseline"] {
|
|
263
|
+
align-items: baseline;
|
|
264
|
+
}
|
|
265
|
+
|
|
200
266
|
// 변경: section 기반 Cell 텍스트는 native cell 토큰과 동일한 스케일을 따르게 한다.
|
|
201
267
|
.table-cell.table-head-cell .table-cell-text {
|
|
202
268
|
color: var(--table-th-text-color);
|