@uniai-fe/uds-primitives 0.3.17 → 0.3.19

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.
Files changed (73) hide show
  1. package/dist/styles.css +224 -160
  2. package/package.json +18 -9
  3. package/src/components/alternate/styles/alternate.scss +11 -8
  4. package/src/components/badge/markup/Badge.tsx +3 -5
  5. package/src/components/badge/styles/index.scss +2 -1
  6. package/src/components/button/index.tsx +7 -1
  7. package/src/components/button/markup/Base.tsx +5 -10
  8. package/src/components/button/markup/Label.tsx +23 -0
  9. package/src/components/button/markup/index.ts +1 -0
  10. package/src/components/button/types/index.ts +1 -0
  11. package/src/components/button/types/label.ts +9 -0
  12. package/src/components/checkbox/markup/Checkbox.tsx +9 -4
  13. package/src/components/checkbox/styles/index.scss +6 -4
  14. package/src/components/chip/index.tsx +1 -2
  15. package/src/components/chip/markup/Chip.tsx +39 -25
  16. package/src/components/chip/markup/DefaultStyle.tsx +76 -48
  17. package/src/components/chip/markup/InputStyle.tsx +71 -48
  18. package/src/components/chip/markup/Label.tsx +15 -0
  19. package/src/components/chip/markup/ListRoot.tsx +88 -0
  20. package/src/components/chip/markup/RemoveButton.tsx +4 -1
  21. package/src/components/chip/markup/index.tsx +13 -1
  22. package/src/components/chip/styles/chip.scss +43 -15
  23. package/src/components/chip/types/options.ts +22 -14
  24. package/src/components/chip/types/props-internal.ts +15 -51
  25. package/src/components/chip/types/props.ts +127 -46
  26. package/src/components/chip/utils/index.ts +1 -1
  27. package/src/components/form/markup/form-field/Header.tsx +3 -1
  28. package/src/components/input/markup/file/UploadedChip.tsx +5 -5
  29. package/src/components/input/styles/foundation.scss +15 -0
  30. package/src/components/input/styles/variables.scss +16 -3
  31. package/src/components/input/types/file.ts +1 -1
  32. package/src/components/radio/markup/Radio.tsx +9 -4
  33. package/src/components/radio/styles/index.scss +6 -4
  34. package/src/components/segmented-control/markup/Label.tsx +22 -0
  35. package/src/components/segmented-control/markup/List.tsx +2 -3
  36. package/src/components/segmented-control/markup/index.ts +1 -0
  37. package/src/components/segmented-control/styles/index.scss +4 -4
  38. package/src/components/segmented-control/types/index.ts +9 -0
  39. package/src/components/select/markup/foundation/Base.tsx +3 -8
  40. package/src/components/select/markup/foundation/Selected.tsx +3 -2
  41. package/src/components/select/markup/multiple/Multiple.tsx +143 -9
  42. package/src/components/select/styles/select.scss +1 -1
  43. package/src/components/select/styles/variables.scss +13 -12
  44. package/src/components/select/types/props.ts +21 -2
  45. package/src/components/slot/index.tsx +2 -6
  46. package/src/components/slot/markup/Text.tsx +34 -0
  47. package/src/components/slot/markup/index.tsx +7 -0
  48. package/src/components/slot/types/index.ts +2 -0
  49. package/src/components/slot/types/text.ts +24 -0
  50. package/src/components/table/markup/foundation/Cell.tsx +4 -12
  51. package/src/components/table/markup/foundation/Td.tsx +4 -7
  52. package/src/components/table/markup/foundation/Text.tsx +16 -0
  53. package/src/components/table/markup/foundation/Th.tsx +4 -7
  54. package/src/components/table/markup/foundation/index.tsx +2 -0
  55. package/src/components/table/styles/foundation.scss +384 -310
  56. package/src/components/table/types/foundation.ts +9 -0
  57. package/src/components/tooltip/markup/Message.tsx +3 -1
  58. package/src/components/tooltip/markup/Text.tsx +21 -0
  59. package/src/components/tooltip/markup/index.tsx +3 -0
  60. package/src/components/tooltip/types/index.ts +1 -0
  61. package/src/components/tooltip/types/text.ts +9 -0
  62. package/src/index.scss +0 -1
  63. package/src/index.tsx +0 -1
  64. package/src/components/chip/utils/class-name.ts +0 -36
  65. package/src/components/label/hooks/index.ts +0 -4
  66. package/src/components/label/img/.gitkeep +0 -0
  67. package/src/components/label/index.scss +0 -1
  68. package/src/components/label/index.tsx +0 -4
  69. package/src/components/label/markup/index.tsx +0 -4
  70. package/src/components/label/styles/index.scss +0 -0
  71. package/src/components/label/types/index.ts +0 -4
  72. package/src/components/label/utils/index.ts +0 -4
  73. /package/src/components/slot/markup/{Component.tsx → Base.tsx} +0 -0
@@ -8,6 +8,7 @@ import { Dropdown } from "../../../dropdown/markup";
8
8
  import type { DropdownSize } from "../../../dropdown/types";
9
9
  import type { SelectMultipleComponentProps } from "../../types/props";
10
10
  import type { SelectMultipleTag } from "../../types/multiple";
11
+ import type { SelectDropdownOption } from "../../types/option";
11
12
  import { SelectMultipleSelectedChip } from "./SelectedChip";
12
13
  import { SelectTriggerBase, SelectTriggerSelected } from "../foundation";
13
14
  import { useSelectDropdownOpenState } from "../../hooks";
@@ -15,6 +16,8 @@ import { useSelectDropdownOpenState } from "../../hooks";
15
16
  const isSameIdList = (previousIds: string[], nextIds: string[]) =>
16
17
  previousIds.length === nextIds.length &&
17
18
  previousIds.every((selectedId, index) => selectedId === nextIds[index]);
19
+ // 변경: synthetic "전체" 옵션 id의 기본 키다. 실제 options id와 충돌하면 suffix를 붙여 회피한다.
20
+ const SELECT_MULTIPLE_ALL_OPTION_BASE_ID = "__select_multiple_all__";
18
21
 
19
22
  /**
20
23
  * Select trigger for multi select; 선택된 tag들을 chip 형태로 렌더링한다.
@@ -44,6 +47,8 @@ const isSameIdList = (previousIds: string[], nextIds: string[]) =>
44
47
  * @param {boolean} [props.open] controlled open 상태
45
48
  * @param {boolean} [props.defaultOpen] uncontrolled 초기 open 상태
46
49
  * @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 콜백
50
+ * @param {boolean} [props.showSelectAllOption] dropdown 첫 번째에 "전체" 옵션 노출 여부
51
+ * @param {React.ReactNode} [props.selectAllLabel="전체"] 전체 옵션 라벨
47
52
  */
48
53
  const SelectMultipleTrigger = forwardRef<
49
54
  HTMLElement,
@@ -76,6 +81,8 @@ const SelectMultipleTrigger = forwardRef<
76
81
  open,
77
82
  defaultOpen,
78
83
  onOpenChange,
84
+ showSelectAllOption,
85
+ selectAllLabel = "전체",
79
86
  ...rest
80
87
  },
81
88
  ref,
@@ -104,6 +111,39 @@ const SelectMultipleTrigger = forwardRef<
104
111
  () => new Map(options.map(option => [option.id, option])),
105
112
  [options],
106
113
  );
114
+ // 변경: 전체 선택 대상은 disabled option을 제외한 "선택 가능 옵션"으로 한정한다.
115
+ const selectableOptions = useMemo(
116
+ () => options.filter(option => !option.disabled),
117
+ [options],
118
+ );
119
+ // 변경: 전체 선택 토글 계산을 위해 선택 가능 옵션 id 배열을 별도로 보관한다.
120
+ const selectableOptionIds = useMemo(
121
+ () => selectableOptions.map(option => option.id),
122
+ [selectableOptions],
123
+ );
124
+ // 변경: 전체 해제 시 빠른 membership 체크를 위해 Set 형태도 함께 보관한다.
125
+ const selectableOptionIdSet = useMemo(
126
+ () => new Set(selectableOptionIds),
127
+ [selectableOptionIds],
128
+ );
129
+ const allOptionId = useMemo(() => {
130
+ // 현재 전달된 옵션 id 집합을 먼저 만든다.
131
+ const existingIdSet = new Set(options.map(option => option.id));
132
+ // "__select_multiple_all__"가 비어 있으면 그대로 사용한다.
133
+ if (!existingIdSet.has(SELECT_MULTIPLE_ALL_OPTION_BASE_ID)) {
134
+ return SELECT_MULTIPLE_ALL_OPTION_BASE_ID;
135
+ }
136
+
137
+ // 충돌 시 "__select_multiple_all___1", "_2" 형태로 suffix를 증가시키며 빈 id를 찾는다.
138
+ let offset = 1;
139
+ let nextId = `${SELECT_MULTIPLE_ALL_OPTION_BASE_ID}_${offset}`;
140
+ while (existingIdSet.has(nextId)) {
141
+ offset += 1;
142
+ nextId = `${SELECT_MULTIPLE_ALL_OPTION_BASE_ID}_${offset}`;
143
+ }
144
+ // 최종적으로 충돌하지 않는 synthetic all-option id를 반환한다.
145
+ return nextId;
146
+ }, [options]);
107
147
 
108
148
  /**
109
149
  * 4) uncontrolled 초기 선택값 계산
@@ -154,17 +194,46 @@ const SelectMultipleTrigger = forwardRef<
154
194
  * - controlled: selectedOptionIds 우선
155
195
  * - uncontrolled: 내부 state 사용
156
196
  */
157
- const resolvedSelectedIds = useMemo(
158
- () =>
159
- isSelectionControlled
160
- ? (selectedOptionIds ?? [])
161
- : uncontrolledSelectedOptionIds,
162
- [isSelectionControlled, selectedOptionIds, uncontrolledSelectedOptionIds],
163
- );
197
+ const resolvedSelectedIds = useMemo(() => {
198
+ const sourceSelectedIds = isSelectionControlled
199
+ ? (selectedOptionIds ?? [])
200
+ : uncontrolledSelectedOptionIds;
201
+
202
+ // 변경: 실제 option id만 유지해 내부 synthetic all-option id가 상태에 저장되지 않도록 차단한다.
203
+ // 이 필터 덕분에 최종 선택 상태는 "실 데이터 옵션"만 source of truth로 유지된다.
204
+ return sourceSelectedIds.filter(selectedId => optionMap.has(selectedId));
205
+ }, [
206
+ isSelectionControlled,
207
+ optionMap,
208
+ selectedOptionIds,
209
+ uncontrolledSelectedOptionIds,
210
+ ]);
164
211
  const selectedIdSet = useMemo(
165
212
  () => new Set(resolvedSelectedIds),
166
213
  [resolvedSelectedIds],
167
214
  );
215
+ const isAllSelectableOptionsSelected = useMemo(
216
+ // 선택 가능 옵션이 1개 이상이고, 그 id가 전부 selectedIdSet에 포함될 때만 true다.
217
+ () =>
218
+ selectableOptionIds.length > 0 &&
219
+ selectableOptionIds.every(selectableId =>
220
+ selectedIdSet.has(selectableId),
221
+ ),
222
+ [selectableOptionIds, selectedIdSet],
223
+ );
224
+ const selectAllOption = useMemo<SelectDropdownOption>(
225
+ // 변경: "전체" 옵션은 렌더링을 위한 synthetic option 객체다.
226
+ // 실제 options 배열에 저장하지 않고 render 단계에서만 주입한다.
227
+ () => ({
228
+ id: allOptionId,
229
+ value: allOptionId,
230
+ label: selectAllLabel,
231
+ multiple: true,
232
+ // 선택 가능한 옵션이 하나도 없으면 "전체" 항목도 disabled로 처리한다.
233
+ disabled: selectableOptionIds.length === 0,
234
+ }),
235
+ [allOptionId, selectAllLabel, selectableOptionIds.length],
236
+ );
168
237
 
169
238
  /**
170
239
  * 8) 표시 라벨 계산(보조)
@@ -248,6 +317,16 @@ const SelectMultipleTrigger = forwardRef<
248
317
  */
249
318
  const panelSize = (dropdownSize ?? size) as DropdownSize;
250
319
  const hasOptions = options.length > 0;
320
+ // "전체" 항목은 옵션이 존재할 때만 의미가 있으므로 hasOptions와 함께 gating한다.
321
+ const shouldRenderSelectAllOption = Boolean(
322
+ showSelectAllOption && hasOptions,
323
+ );
324
+ const renderedOptions = useMemo(
325
+ // 변경: all-option은 dropdown 첫 행 고정 요구사항에 맞춰 항상 배열 맨 앞에 주입한다.
326
+ () =>
327
+ shouldRenderSelectAllOption ? [selectAllOption, ...options] : options,
328
+ [options, selectAllOption, shouldRenderSelectAllOption],
329
+ );
251
330
  const MAX_VISIBLE_TAGS = 3;
252
331
  const visibleTags = hasTags ? derivedTags.slice(0, MAX_VISIBLE_TAGS) : [];
253
332
  const overflowCount = hasTags
@@ -262,6 +341,55 @@ const SelectMultipleTrigger = forwardRef<
262
341
  * - legacy onOptionSelect는 하위호환으로 유지
263
342
  */
264
343
  const handleOptionSelect = (optionId: string) => {
344
+ // 현재 클릭된 옵션이 synthetic all-option인지 먼저 판별한다.
345
+ const isSelectAllOption =
346
+ shouldRenderSelectAllOption && optionId === allOptionId;
347
+ if (isSelectAllOption) {
348
+ // all-option 클릭 분기
349
+ // - 이미 전체 선택 상태면: selectable option들만 제거(전체 해제)
350
+ // - 일부 선택 상태면: selectable option들을 전부 합집합으로 추가(전체 선택)
351
+ const nextSelectedOptionIds = isAllSelectableOptionsSelected
352
+ ? resolvedSelectedIds.filter(
353
+ selectedId => !selectableOptionIdSet.has(selectedId),
354
+ )
355
+ : Array.from(
356
+ new Set([...resolvedSelectedIds, ...selectableOptionIds]),
357
+ );
358
+ // 최종 id 배열을 실제 option 객체 배열로 역매핑해 payload 계약을 유지한다.
359
+ const nextSelectedOptions = nextSelectedOptionIds
360
+ .map(selectedId => optionMap.get(selectedId))
361
+ .filter(
362
+ (
363
+ selectedOption,
364
+ ): selectedOption is NonNullable<typeof selectedOption> =>
365
+ Boolean(selectedOption),
366
+ );
367
+
368
+ // uncontrolled 모드에서는 내부 state를 즉시 동기화한다.
369
+ if (!isSelectionControlled) {
370
+ setUncontrolledSelectedOptionIds(nextSelectedOptionIds);
371
+ }
372
+
373
+ // onChange payload는 기존 multiple 계약을 그대로 사용한다.
374
+ // currentOption은 synthetic all-option으로 전달해 "전체 선택 액션"임을 호출부에서 구분 가능하게 한다.
375
+ onChange?.({
376
+ mode: "multiple",
377
+ selectedOptionIds: nextSelectedOptionIds,
378
+ selectedValues: nextSelectedOptions.map(
379
+ selectedOption => selectedOption.value,
380
+ ),
381
+ selectedOptions: nextSelectedOptions,
382
+ currentOption: selectAllOption,
383
+ isSelected: !isAllSelectableOptionsSelected,
384
+ });
385
+
386
+ // legacy 하위호환 콜백도 동일하게 호출한다.
387
+ onOptionSelect?.(selectAllOption);
388
+ return;
389
+ }
390
+
391
+ // 일반 옵션 클릭 분기
392
+ // 이미 선택된 id면 제거, 미선택 id면 추가하는 기본 multiple toggle 로직이다.
265
393
  const wasSelected = selectedIdSet.has(optionId);
266
394
  const nextSelectedOptionIds = wasSelected
267
395
  ? resolvedSelectedIds.filter(selectedId => selectedId !== optionId)
@@ -370,7 +498,7 @@ const SelectMultipleTrigger = forwardRef<
370
498
  {hasOptions ? (
371
499
  <>
372
500
  {/* multi select 전용 옵션을 Dropdown.Menu.Item으로 노출한다. */}
373
- {options.map(option => (
501
+ {renderedOptions.map(option => (
374
502
  <Dropdown.Menu.Item
375
503
  key={option.id}
376
504
  label={option.label}
@@ -379,7 +507,13 @@ const SelectMultipleTrigger = forwardRef<
379
507
  left={option.left}
380
508
  right={option.right}
381
509
  multiple
382
- isSelected={selectedIdSet.has(option.id)}
510
+ isSelected={
511
+ // synthetic all-option의 checked 상태는 "전체가 선택되었는가"로 계산한다.
512
+ // 일반 option은 기존 selectedIdSet membership으로 계산한다.
513
+ option.id === allOptionId
514
+ ? isAllSelectableOptionsSelected
515
+ : selectedIdSet.has(option.id)
516
+ }
383
517
  onSelect={event => {
384
518
  if (option.disabled || isInteractionBlocked) {
385
519
  event.preventDefault();
@@ -331,7 +331,7 @@
331
331
  }
332
332
 
333
333
  .select-button[data-priority="secondary"][data-size="large"] .select-label {
334
- font-weight: 600;
334
+ --select-text-large-weight: 600;
335
335
  }
336
336
 
337
337
  .select-tags {
@@ -66,18 +66,19 @@
66
66
  --input-table-text-large-line-height
67
67
  );
68
68
  --select-table-text-large-weight: var(--input-table-text-large-weight);
69
- --select-text-small-size: 15px;
70
- --select-text-small-line-height: 1.4;
71
- --select-text-small-letter-spacing: 0.2px;
72
- --select-text-small-weight: 400;
73
- --select-text-medium-size: var(--font-body-medium-size);
74
- --select-text-medium-line-height: var(--font-body-medium-line-height);
75
- --select-text-medium-letter-spacing: 0;
76
- --select-text-medium-weight: 400;
77
- --select-text-large-size: var(--font-body-large-size);
78
- --select-text-large-line-height: var(--font-body-large-line-height);
79
- --select-text-large-letter-spacing: 0;
80
- --select-text-large-weight: 400;
69
+ /* 변경: trigger 텍스트 토큰을 input text 토큰과 직접 매핑해 size typography를 동기화한다. */
70
+ --select-text-small-size: var(--input-text-small-size);
71
+ --select-text-small-line-height: var(--input-text-small-line-height);
72
+ --select-text-small-letter-spacing: var(--input-text-small-letter-spacing);
73
+ --select-text-small-weight: var(--input-text-small-weight);
74
+ --select-text-medium-size: var(--input-text-medium-size);
75
+ --select-text-medium-line-height: var(--input-text-medium-line-height);
76
+ --select-text-medium-letter-spacing: var(--input-text-medium-letter-spacing);
77
+ --select-text-medium-weight: var(--input-text-medium-weight);
78
+ --select-text-large-size: var(--input-text-large-size);
79
+ --select-text-large-line-height: var(--input-text-large-line-height);
80
+ --select-text-large-letter-spacing: var(--input-text-large-letter-spacing);
81
+ --select-text-large-weight: var(--input-text-large-weight);
81
82
 
82
83
  --select-icon-size-small: 1.6rem;
83
84
  --select-icon-size-medium: 2rem;
@@ -257,9 +257,25 @@ export type SelectDefaultComponentProps = SelectTriggerDefaultProps &
257
257
  SelectDropdownBehaviorProps &
258
258
  SelectWidthOption;
259
259
 
260
+ /**
261
+ * Select.Multiple 전체 선택 옵션 props
262
+ * @property {boolean} [showSelectAllOption] dropdown 첫 행에 "전체" 옵션 노출 여부
263
+ * @property {ReactNode} [selectAllLabel] "전체" 옵션 라벨 커스터마이징
264
+ */
265
+ export interface SelectMultipleAllOptionProps {
266
+ /**
267
+ * dropdown 첫 행에 "전체" 옵션 노출 여부
268
+ */
269
+ showSelectAllOption?: boolean;
270
+ /**
271
+ * "전체" 옵션 라벨 커스터마이징
272
+ */
273
+ selectAllLabel?: ReactNode;
274
+ }
275
+
260
276
  /**
261
277
  * Select.Multiple 컴포넌트 props
262
- * @typedef {SelectTriggerMultipleProps & SelectDropdownConfigProps & SelectDropdownBehaviorProps & SelectWidthOption} SelectMultipleComponentProps
278
+ * @typedef {SelectTriggerMultipleProps & SelectDropdownConfigProps & SelectDropdownBehaviorProps & SelectWidthOption & SelectMultipleAllOptionProps} SelectMultipleComponentProps
263
279
  * @property {ReactNode} [displayLabel] 선택된 라벨
264
280
  * @property {ReactNode} [placeholder] placeholder 텍스트
265
281
  * @property {SelectMultipleTag[]} [tags] multi select tag 리스트
@@ -284,8 +300,11 @@ export type SelectDefaultComponentProps = SelectTriggerDefaultProps &
284
300
  * @property {boolean} [open] dropdown open 상태
285
301
  * @property {boolean} [defaultOpen] uncontrolled 초기 open 상태
286
302
  * @property {(open: boolean) => void} [onOpenChange] open state change 콜백
303
+ * @property {boolean} [showSelectAllOption] dropdown 첫 행에 "전체" 옵션 노출 여부
304
+ * @property {ReactNode} [selectAllLabel] "전체" 옵션 라벨 커스터마이징
287
305
  */
288
306
  export type SelectMultipleComponentProps = SelectTriggerMultipleProps &
289
307
  SelectDropdownConfigProps &
290
308
  SelectDropdownBehaviorProps &
291
- SelectWidthOption;
309
+ SelectWidthOption &
310
+ SelectMultipleAllOptionProps;
@@ -1,6 +1,2 @@
1
- export { default as SlotComponent } from "./markup/Component";
2
- export type {
3
- SlotComponentProps,
4
- SlotComponentType,
5
- PolymorphicRef,
6
- } from "./types/props";
1
+ export * from "./markup";
2
+ export type * from "./types";
@@ -0,0 +1,34 @@
1
+ import type { ElementType } from "react";
2
+ import clsx from "clsx";
3
+ import SlotBase from "./Base";
4
+ import type { SlotTextProps } from "../types/text";
5
+
6
+ /**
7
+ * SlotText; 텍스트 역할 children만 래핑하는 공용 슬롯.
8
+ * @component
9
+ * @param {SlotTextProps} props
10
+ * @param {React.ElementType} [props.as="span"] 렌더링할 요소.
11
+ * @param {React.ReactNode} [props.children] 문자열/숫자는 래핑하고, 그 외 ReactNode는 그대로 반환한다.
12
+ * @param {string} [props.className] 래핑 시 적용할 className.
13
+ */
14
+ export default function SlotText<C extends ElementType = "span">({
15
+ as = "span" as C,
16
+ children,
17
+ className,
18
+ ...restProps
19
+ }: SlotTextProps<C>) {
20
+ if (!["string", "number"].includes(typeof children)) {
21
+ return children;
22
+ }
23
+
24
+ // 문자열/숫자 children만 공통 slot text 마크업으로 감싼다.
25
+ return (
26
+ <SlotBase
27
+ as={as as ElementType}
28
+ className={clsx("slot-text", className)}
29
+ {...restProps}
30
+ >
31
+ {children}
32
+ </SlotBase>
33
+ );
34
+ }
@@ -0,0 +1,7 @@
1
+ import SlotBase from "./Base";
2
+ import SlotText from "./Text";
3
+
4
+ export const Slot = {
5
+ Base: SlotBase,
6
+ Text: SlotText,
7
+ };
@@ -0,0 +1,2 @@
1
+ export type * from "./props";
2
+ export type * from "./text";
@@ -0,0 +1,24 @@
1
+ import type { ElementType, ReactNode } from "react";
2
+ import type { SlotComponentRestProps } from "./props";
3
+
4
+ /**
5
+ * SlotTextProps; 텍스트 역할 children 래핑용 공통 props.
6
+ * @property {React.ElementType} [as] 렌더링할 요소. 기본값은 span.
7
+ * @property {React.ReactNode} [children] 문자열/숫자는 래핑하고 그 외 ReactNode는 그대로 반환한다.
8
+ * @property {string} [className] 텍스트 래퍼 className.
9
+ */
10
+ export type SlotTextProps<C extends ElementType = "span"> =
11
+ SlotComponentRestProps<C> & {
12
+ /**
13
+ * 렌더링할 요소.
14
+ */
15
+ as?: C;
16
+ /**
17
+ * 텍스트/노드 children.
18
+ */
19
+ children?: ReactNode;
20
+ /**
21
+ * 텍스트 래퍼 className.
22
+ */
23
+ className?: string;
24
+ };
@@ -1,6 +1,7 @@
1
1
  import clsx from "clsx";
2
2
  import { forwardRef } from "react";
3
3
  import type { TableCellContentProps } from "../../types";
4
+ import TableText from "./Text";
4
5
 
5
6
  /**
6
7
  * Table Foundation; Cell 콘텐츠 래퍼 컴포넌트
@@ -48,18 +49,9 @@ const TableCell = forwardRef<HTMLDivElement, TableCellContentProps>(
48
49
  )}
49
50
  >
50
51
  {/* 변경: 텍스트 콘텐츠는 slot 전용 className으로 감싸 스타일 적용 범위를 고정한다. */}
51
- {typeof children === "string" || typeof children === "number" ? (
52
- <span
53
- className={clsx(
54
- "table-cell-text",
55
- section && `table-${section}-cell-text`,
56
- )}
57
- >
58
- {children}
59
- </span>
60
- ) : (
61
- children
62
- )}
52
+ <TableText className={clsx(section && `table-${section}-cell-text`)}>
53
+ {children}
54
+ </TableText>
63
55
  </div>
64
56
  );
65
57
  },
@@ -1,6 +1,7 @@
1
1
  import clsx from "clsx";
2
2
  import { forwardRef } from "react";
3
3
  import type { TableTdProps } from "../../types";
4
+ import TableText from "./Text";
4
5
 
5
6
  /**
6
7
  * Table Foundation; Td 마크업 컴포넌트
@@ -20,13 +21,9 @@ const TableTd = forwardRef<HTMLTableCellElement, TableTdProps>(
20
21
  style={{ ...style, textAlign: "left" }}
21
22
  >
22
23
  {/* 변경: 태그 셀렉터 의존을 피하기 위해 className 기반 텍스트 노드를 사용한다. */}
23
- {typeof children === "string" || typeof children === "number" ? (
24
- <span className={clsx("table-native-cell-text", "table-td-text")}>
25
- {children}
26
- </span>
27
- ) : (
28
- children
29
- )}
24
+ <TableText className={clsx("table-native-cell-text", "table-td-text")}>
25
+ {children}
26
+ </TableText>
30
27
  </td>
31
28
  );
32
29
  },
@@ -0,0 +1,16 @@
1
+ import clsx from "clsx";
2
+ import { Slot } from "../../../slot";
3
+ import type { TableTextProps } from "../../types";
4
+
5
+ /**
6
+ * Table Text; th/td/cell 텍스트 공통 렌더 컴포넌트.
7
+ * @component
8
+ * @param {TableTextProps} props
9
+ * @param {React.ReactNode} [props.children] 텍스트 콘텐츠
10
+ * @param {string} [props.className] table text className
11
+ */
12
+ export default function TableText({ className, ...restProps }: TableTextProps) {
13
+ return (
14
+ <Slot.Text className={clsx("table-cell-text", className)} {...restProps} />
15
+ );
16
+ }
@@ -1,6 +1,7 @@
1
1
  import clsx from "clsx";
2
2
  import { forwardRef } from "react";
3
3
  import type { TableThProps } from "../../types";
4
+ import TableText from "./Text";
4
5
 
5
6
  /**
6
7
  * Table Foundation; Th 마크업 컴포넌트
@@ -29,13 +30,9 @@ const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
29
30
  style={style}
30
31
  >
31
32
  {/* 변경: 태그 셀렉터 의존을 피하기 위해 className 기반 텍스트 노드를 사용한다. */}
32
- {typeof children === "string" || typeof children === "number" ? (
33
- <span className={clsx("table-native-cell-text", "table-th-text")}>
34
- {children}
35
- </span>
36
- ) : (
37
- children
38
- )}
33
+ <TableText className={clsx("table-native-cell-text", "table-th-text")}>
34
+ {children}
35
+ </TableText>
39
36
  </th>
40
37
  );
41
38
  },
@@ -8,6 +8,7 @@ import TableTh from "./Th";
8
8
  import TableRoot from "./Root";
9
9
  import TableRow from "./Row";
10
10
  import TableTd from "./Td";
11
+ import TableText from "./Text";
11
12
 
12
13
  /**
13
14
  * Table Foundation; 기초 마크업 컴포넌트 세트
@@ -27,4 +28,5 @@ export const TableFoundation = {
27
28
  Tr: TableRow,
28
29
  Th: TableTh,
29
30
  Td: TableTd,
31
+ Text: TableText,
30
32
  };