@uniai-fe/uds-primitives 0.3.13 → 0.3.15

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 CHANGED
@@ -363,7 +363,8 @@
363
363
  --input-text-disabled-color: var(--color-label-disabled);
364
364
  --input-placeholder-color: var(--color-label-alternative);
365
365
  --input-placeholder-disabled-color: var(--color-label-disabled);
366
- --input-placeholder-readonly-color: var(--input-placeholder-disabled-color);
366
+ /* 변경: readonly 입력은 placeholder를 숨겨 value 텍스트 대비를 고정한다. */
367
+ --input-placeholder-readonly-color: transparent;
367
368
  --input-font-size: var(--font-body-medium-size);
368
369
  --input-line-height: var(--font-body-medium-line-height);
369
370
  --input-font-weight: 400;
@@ -472,6 +473,12 @@
472
473
  --select-table-color-text: var(--input-text-color);
473
474
  --select-table-color-text-readonly: var(--select-primary-color-text-readonly);
474
475
  --select-table-color-placeholder: var(--input-placeholder-color);
476
+ --select-table-color-placeholder-disabled: var(
477
+ --select-primary-color-placeholder-disabled
478
+ );
479
+ --select-table-color-placeholder-readonly: var(
480
+ --select-primary-color-placeholder-readonly
481
+ );
475
482
  --select-table-icon-color-default: var(--select-icon-color-default);
476
483
  --select-table-icon-color-focused: var(--select-icon-color-focused);
477
484
  --select-table-icon-color-disabled: var(--select-icon-color-disabled);
@@ -506,6 +513,11 @@
506
513
  --select-icon-size-medium: 2rem;
507
514
  --select-icon-size-large: 2rem;
508
515
  --select-primary-color-placeholder: var(--input-placeholder-color);
516
+ /* 변경: placeholder disabled/readonly 토큰을 분리해 상태별 제어 지점을 고정한다. */
517
+ --select-primary-color-placeholder-disabled: var(--color-label-disabled);
518
+ --select-primary-color-placeholder-readonly: var(
519
+ --select-primary-color-placeholder-disabled
520
+ );
509
521
  --select-primary-color-surface: var(--input-surface-color);
510
522
  --select-primary-color-text: var(--color-label-alternative);
511
523
  --select-primary-color-text-focused: var(--color-label-strong);
@@ -2550,6 +2562,10 @@ figure.chip {
2550
2562
  border-width: var(--input-border-width-default);
2551
2563
  background-color: var(--input-surface-disabled-color);
2552
2564
  }
2565
+ .input-field:not([data-priority=secondary])[data-state=error][data-readonly=true] {
2566
+ border-color: var(--input-border-error-color);
2567
+ border-width: var(--input-border-width-emphasis);
2568
+ }
2553
2569
 
2554
2570
  .input-element {
2555
2571
  flex: 1 1 auto;
@@ -3806,12 +3822,24 @@ figure.chip {
3806
3822
  .select-button[data-priority=table] .select-label-placeholder {
3807
3823
  color: var(--select-table-color-placeholder);
3808
3824
  }
3809
- .select-button[data-state=disabled] .select-label-placeholder, .select-button[data-readonly=true] .select-label-placeholder {
3810
- color: var(--select-primary-color-text-disabled);
3825
+ .select-button[data-state=disabled] .select-label-placeholder {
3826
+ color: var(--select-primary-color-placeholder-disabled);
3827
+ }
3828
+ .select-button[data-readonly=true] .select-label-placeholder {
3829
+ color: var(--select-primary-color-placeholder-readonly);
3811
3830
  }
3812
- .select-button[data-priority=secondary][data-state=disabled] .select-label-placeholder, .select-button[data-priority=secondary][data-readonly=true] .select-label-placeholder {
3831
+ .select-button[data-priority=secondary][data-state=disabled] .select-label-placeholder {
3813
3832
  color: var(--select-secondary-color-placeholder-disabled);
3814
3833
  }
3834
+ .select-button[data-priority=secondary][data-readonly=true] .select-label-placeholder {
3835
+ color: var(--select-secondary-color-placeholder-readonly);
3836
+ }
3837
+ .select-button[data-priority=table][data-state=disabled] .select-label-placeholder {
3838
+ color: var(--select-table-color-placeholder-disabled);
3839
+ }
3840
+ .select-button[data-priority=table][data-readonly=true] .select-label-placeholder {
3841
+ color: var(--select-table-color-placeholder-readonly);
3842
+ }
3815
3843
 
3816
3844
  .select-button[data-size=small] .select-label {
3817
3845
  font-size: var(--select-text-small-size);
@@ -4193,17 +4221,21 @@ figure.chip {
4193
4221
  font-weight: 600;
4194
4222
  }
4195
4223
 
4224
+ .table.table-container[data-layout=line] .table-native-cell[rowspan] {
4225
+ border-right: 1px solid var(--table-border-color);
4226
+ }
4227
+
4196
4228
  .table.table-container[data-layout=grid] .table-native-cell.table-th,
4197
4229
  .table.table-container[data-layout=grid] .table-native-cell.table-td {
4198
4230
  --table-cell-padding-inline: var(--table-grid-cell-padding-inline);
4199
4231
  --table-cell-padding-block: var(--table-grid-cell-padding-block);
4200
- border: 1px solid var(--table-border-color);
4201
4232
  height: var(--table-grid-cell-height);
4202
4233
  }
4203
4234
 
4204
4235
  .table.table-container[data-layout=grid] {
4205
4236
  border-radius: var(--table-grid-border-radius);
4206
- overflow: hidden;
4237
+ overflow: visible;
4238
+ isolation: isolate;
4207
4239
  }
4208
4240
 
4209
4241
  .table.table-container[data-layout=grid] .table-head .table-native-cell.table-th {
@@ -4214,11 +4246,117 @@ figure.chip {
4214
4246
  background-color: var(--table-grid-row-header-background-color);
4215
4247
  }
4216
4248
 
4249
+ .table.table-container[data-layout=grid] .table-foot .table-native-cell.table-th {
4250
+ background-color: var(--table-grid-row-header-background-color);
4251
+ }
4252
+
4217
4253
  .table.table-container[data-layout=grid] .table-row[data-highlighted=true] .table-native-cell.table-th,
4218
4254
  .table.table-container[data-layout=grid] .table-row[data-highlighted=true] .table-native-cell.table-td {
4219
4255
  background-color: var(--table-grid-row-highlight-background-color);
4220
4256
  }
4221
4257
 
4258
+ .table.table-container .table-row > .table-native-cell.table-th:first-child {
4259
+ position: sticky;
4260
+ left: 0;
4261
+ }
4262
+
4263
+ .table.table-container .table-head .table-row > .table-native-cell.table-th:first-child {
4264
+ z-index: 150;
4265
+ }
4266
+
4267
+ .table.table-container .table-body .table-row > .table-native-cell.table-th:first-child {
4268
+ z-index: 100;
4269
+ }
4270
+
4271
+ .table.table-container .table-foot .table-row > .table-native-cell.table-th:first-child {
4272
+ z-index: 50;
4273
+ }
4274
+
4275
+ .table.table-container .table-head .table-row:first-child > .table-native-cell.table-th:first-child {
4276
+ z-index: 200;
4277
+ }
4278
+
4279
+ .table.table-container .table-native-cell.table-td {
4280
+ position: relative;
4281
+ z-index: 0;
4282
+ }
4283
+
4284
+ .table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell:first-child {
4285
+ border-top-left-radius: var(--table-grid-border-radius);
4286
+ }
4287
+
4288
+ .table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell:last-child {
4289
+ border-top-right-radius: var(--table-grid-border-radius);
4290
+ }
4291
+
4292
+ .table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:first-child,
4293
+ .table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:first-child {
4294
+ border-bottom-left-radius: var(--table-grid-border-radius);
4295
+ }
4296
+
4297
+ .table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:last-child,
4298
+ .table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:last-child {
4299
+ border-bottom-right-radius: var(--table-grid-border-radius);
4300
+ }
4301
+
4302
+ .table.table-container[data-layout=grid] .table-cell-content {
4303
+ border-right: 1px solid var(--table-border-color);
4304
+ border-bottom: 1px solid var(--table-border-color);
4305
+ }
4306
+
4307
+ .table.table-container[data-layout=grid] .table-head .table-row:first-child > .table-native-cell > .table-cell-content {
4308
+ border-top: 1px solid var(--table-border-color);
4309
+ }
4310
+
4311
+ .table.table-container[data-layout=grid] .table-row > .table-native-cell:first-child > .table-cell-content {
4312
+ border-left: 1px solid var(--table-border-color);
4313
+ }
4314
+
4315
+ .table.table-container[data-layout=grid] .table-row > .table-native-cell[rowspan] > .table-cell-content {
4316
+ border-right: 0;
4317
+ }
4318
+
4319
+ .table.table-container[data-layout=grid] .table-row > .table-native-cell[rowspan] + .table-native-cell > .table-cell-content {
4320
+ border-left: 1px solid var(--table-border-color);
4321
+ }
4322
+
4323
+ .table.table-container[data-layout=grid] .table-row:first-child > .table-native-cell:first-child .table-cell-content {
4324
+ border-top-left-radius: inherit;
4325
+ }
4326
+
4327
+ .table.table-container[data-layout=grid] .table-row:first-child > .table-native-cell:last-child .table-cell-content {
4328
+ border-top-right-radius: inherit;
4329
+ }
4330
+
4331
+ .table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:first-child .table-cell-content,
4332
+ .table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:first-child .table-cell-content {
4333
+ border-bottom-left-radius: inherit;
4334
+ }
4335
+
4336
+ .table.table-container[data-layout=grid] .table-foot .table-row:last-child > .table-native-cell:last-child .table-cell-content,
4337
+ .table.table-container[data-layout=grid]:not([data-has-footer=true]) .table-body:last-of-type .table-row:last-child > .table-native-cell:last-child .table-cell-content {
4338
+ border-bottom-right-radius: inherit;
4339
+ }
4340
+
4341
+ .table-scroll-wrapper {
4342
+ width: 100%;
4343
+ border-radius: var(--table-grid-border-radius);
4344
+ }
4345
+
4346
+ .table-scroll-wrapper[data-scroll-axis=x] {
4347
+ overflow-x: auto;
4348
+ overflow-y: visible;
4349
+ }
4350
+
4351
+ .table-scroll-wrapper[data-scroll-axis=y] {
4352
+ overflow-x: visible;
4353
+ overflow-y: auto;
4354
+ }
4355
+
4356
+ .table-scroll-wrapper[data-scroll-axis=both] {
4357
+ overflow: auto;
4358
+ }
4359
+
4222
4360
  .table-native-cell.table-th .table-native-cell-text {
4223
4361
  color: var(--table-th-text-color);
4224
4362
  font-size: var(--table-th-text-size);
@@ -4442,6 +4580,14 @@ figure.chip {
4442
4580
  line-height: var(--table-td-text-line-height);
4443
4581
  }
4444
4582
 
4583
+ .table.table-container .table-foot .table-cell-content,
4584
+ .table.table-container .table-foot .table-cell-text {
4585
+ color: var(--table-td-text-color);
4586
+ font-size: var(--table-td-text-size);
4587
+ font-weight: var(--table-td-text-weight);
4588
+ line-height: var(--table-td-text-line-height);
4589
+ }
4590
+
4445
4591
  .table-cell.table-body-cell .radio-label-text,
4446
4592
  .table-cell.table-foot-cell .radio-label-text {
4447
4593
  color: var(--table-td-text-color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -40,6 +40,7 @@ const INPUT_DATE_TABLE_FORMAT = "YY-MM-DD";
40
40
  * @param {UseFormRegisterReturn} [props.register] RHF register
41
41
  * @param {string} [props.placeholder="YYYY-MM-DD"] placeholder
42
42
  * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] trigger input priority
43
+ * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] trigger input state
43
44
  * @param {ReactNode} [props.header] 패널 header 콘텐츠
44
45
  * @param {ReactNode} [props.footer] 패널 footer 콘텐츠
45
46
  * @param {unknown} [props.timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
@@ -66,6 +67,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
66
67
  register,
67
68
  placeholder = "YYYY-MM-DD",
68
69
  priority = "primary",
70
+ state = "default",
69
71
  className,
70
72
  header,
71
73
  footer,
@@ -156,8 +158,10 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
156
158
  placeholder,
157
159
  displayValue: triggerValue,
158
160
  disabled,
161
+ readOnly,
159
162
  onClick: handleTriggerClick,
160
163
  priority,
164
+ state,
161
165
  };
162
166
 
163
167
  const triggerNode = trigger ?? renderTrigger?.(triggerRenderProps) ?? (
@@ -168,6 +172,8 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
168
172
  placeholder={placeholder}
169
173
  displayValue={triggerValue}
170
174
  disabled={disabled}
175
+ state={state}
176
+ readOnly={readOnly}
171
177
  onClick={handleTriggerClick}
172
178
  priority={priority}
173
179
  />
@@ -1,7 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import clsx from "clsx";
4
- import type { MouseEvent } from "react";
4
+ import type {
5
+ ClipboardEvent,
6
+ DragEvent,
7
+ FormEvent,
8
+ KeyboardEvent,
9
+ MouseEvent,
10
+ } from "react";
5
11
  import { forwardRef } from "react";
6
12
  import { Calendar } from "../../../calendar";
7
13
  import { InputFoundation } from "../foundation";
@@ -18,7 +24,9 @@ import type { InputCalendarTriggerViewProps } from "../../types";
18
24
  * @param {string} [props.displayValue] 표시 문자열
19
25
  * @param {(event: MouseEvent<Element>) => void} [props.onClick] 클릭 핸들러
20
26
  * @param {boolean} [props.disabled] disabled 여부
27
+ * @param {boolean} [props.readOnly] readOnly 여부
21
28
  * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority] trigger input priority
29
+ * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state] trigger input state
22
30
  * @param {string} [props.id] input id
23
31
  * @param {string} [props.name] form name
24
32
  * @param {UseFormRegisterReturn} [props.register] RHF register
@@ -34,7 +42,9 @@ const InputDateTrigger = forwardRef<
34
42
  displayValue,
35
43
  onClick,
36
44
  disabled,
45
+ readOnly,
37
46
  priority,
47
+ state,
38
48
  id,
39
49
  name,
40
50
  register,
@@ -53,6 +63,55 @@ const InputDateTrigger = forwardRef<
53
63
  }
54
64
  onClick?.(event as never);
55
65
  };
66
+ // 변경: Date trigger는 기본적으로 직접 타이핑을 막고, 달력 선택을 단일 입력 경로로 유지한다.
67
+ const shouldBlockTyping = !disabled && !readOnly;
68
+
69
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
70
+ if (!shouldBlockTyping) {
71
+ return;
72
+ }
73
+
74
+ const navigableKeys = new Set([
75
+ "Tab",
76
+ "Shift",
77
+ "Control",
78
+ "Alt",
79
+ "Meta",
80
+ "Escape",
81
+ "ArrowLeft",
82
+ "ArrowRight",
83
+ "ArrowUp",
84
+ "ArrowDown",
85
+ "Home",
86
+ "End",
87
+ "PageUp",
88
+ "PageDown",
89
+ ]);
90
+
91
+ if (navigableKeys.has(event.key)) {
92
+ return;
93
+ }
94
+
95
+ event.preventDefault();
96
+ };
97
+
98
+ const handleBeforeInput = (event: FormEvent<HTMLInputElement>) => {
99
+ if (shouldBlockTyping) {
100
+ event.preventDefault();
101
+ }
102
+ };
103
+
104
+ const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
105
+ if (shouldBlockTyping) {
106
+ event.preventDefault();
107
+ }
108
+ };
109
+
110
+ const handleDrop = (event: DragEvent<HTMLInputElement>) => {
111
+ if (shouldBlockTyping) {
112
+ event.preventDefault();
113
+ }
114
+ };
56
115
 
57
116
  return (
58
117
  <InputFoundation.Base
@@ -60,17 +119,23 @@ const InputDateTrigger = forwardRef<
60
119
  ref={ref}
61
120
  value={displayValue}
62
121
  // PopOver.Trigger(asChild)가 주입한 `type="button"`으로 placeholder가 사라지는 문제를 막기 위해
63
- // Date trigger는 항상 readOnly text input으로 고정한다.
122
+ // Date trigger는 항상 text type을 유지한다.
64
123
  type="text"
65
- readOnly
124
+ readOnly={readOnly}
66
125
  disabled={disabled}
126
+ inputMode={shouldBlockTyping ? "none" : undefined}
67
127
  id={id}
68
128
  name={name ?? register?.name}
69
129
  register={register}
70
130
  priority={priority}
131
+ state={state}
71
132
  placeholder={placeholder}
72
133
  className={clsx("input-date-trigger-input", className)}
73
134
  onClick={handleInputClick}
135
+ onKeyDown={handleKeyDown}
136
+ onBeforeInput={handleBeforeInput}
137
+ onPaste={handlePaste}
138
+ onDrop={handleDrop}
74
139
  // table priority는 icon을 왼쪽 슬롯에 배치한다.
75
140
  left={
76
141
  priority === "table" ? (
@@ -30,9 +30,10 @@ import InputBaseUtil from "./Utility";
30
30
  * @component
31
31
  * @param {InputProps} props Input 컴포넌트 공통 props
32
32
  * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] 디자인 토큰 우선순위
33
- * @param {"small" | "medium" | "large"} [props.size="medium"] 높이/타이포 세트
34
- * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] 시각 상태
33
+ * @param {"small" | "medium" | "large"} [props.size="medium"] 높이/타이포 세트(tertiary는 small 고정)
34
+ * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] 시각 상태(tertiary는 disabled 외 feedback color 미적용)
35
35
  * @param {boolean} [props.block=false] true면 width 100% (`priority="table"`은 width 미지정 시 full 기본)
36
+ * @param {React.ReactNode} [props.inlineLabel] tertiary border 내부 상단 라벨
36
37
  * @param {FormFieldWidth} [props.width] width preset
37
38
  * @param {React.ReactNode} [props.left] 입력 왼쪽 슬롯(아이콘/텍스트)
38
39
  * @param {React.ReactNode} [props.right] 입력 오른쪽 슬롯
@@ -63,6 +64,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
63
64
  state: stateProp = "default",
64
65
  block = false,
65
66
  width,
67
+ inlineLabel,
66
68
  left,
67
69
  right,
68
70
  clear,
@@ -91,6 +93,8 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
91
93
  const isTablePriority = priority === "table";
92
94
  const resolvedBlock = block || (isTablePriority && width === undefined);
93
95
 
96
+ // 변경: tertiary는 디자인 계약상 small 단일 사이즈만 사용한다.
97
+ const resolvedSize = priority === "tertiary" ? "small" : size;
94
98
  const generatedId = useId();
95
99
  const registerRef = register?.ref;
96
100
  const registerOnChange = register?.onChange;
@@ -117,7 +121,15 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
117
121
  const currentState = disabled ? "disabled" : stateProp;
118
122
  const isDisabled =
119
123
  currentState === "disabled" || currentState === "loading";
120
- const visualState = !isDisabled && isFocused ? "active" : currentState;
124
+ // 변경: tertiary는 status feedback(success/error/active/focused) 컬러를 적용하지 않는다.
125
+ const visualState =
126
+ priority === "tertiary"
127
+ ? isDisabled
128
+ ? "disabled"
129
+ : "default"
130
+ : !isDisabled && isFocused
131
+ ? "active"
132
+ : currentState;
121
133
 
122
134
  const setInputRef = useCallback(
123
135
  (node: HTMLInputElement | null) => {
@@ -201,13 +213,13 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
201
213
  className={clsx(
202
214
  "input",
203
215
  `input-priority-${priority}`,
204
- `input-size-${size}`,
216
+ `input-size-${resolvedSize}`,
205
217
  `input-state-${visualState}`,
206
218
  resolvedBlock && "input-block",
207
219
  className,
208
220
  )}
209
221
  data-priority={priority}
210
- data-size={size}
222
+ data-size={resolvedSize}
211
223
  data-state={visualState}
212
224
  data-readonly={isReadOnly ? "true" : undefined}
213
225
  data-block={resolvedBlock ? "true" : undefined}
@@ -219,7 +231,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
219
231
  className={clsx(
220
232
  "input-box",
221
233
  `input-box-priority-${priority}`,
222
- `input-box-size-${size}`,
234
+ `input-box-size-${resolvedSize}`,
223
235
  `input-box-state-${visualState}`,
224
236
  resolvedBlock && "input-box-block",
225
237
  boxClassNameProp,
@@ -230,7 +242,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
230
242
  className="input-field"
231
243
  data-state={visualState}
232
244
  data-priority={priority}
233
- data-size={size}
245
+ data-size={resolvedSize}
234
246
  data-readonly={isReadOnly ? "true" : undefined}
235
247
  data-block={resolvedBlock ? "true" : undefined}
236
248
  >
@@ -239,6 +251,10 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
239
251
  {left && (
240
252
  <InputBaseSideSlot type="left">{left}</InputBaseSideSlot>
241
253
  )}
254
+ {priority === "tertiary" && inlineLabel ? (
255
+ // 변경: tertiary 라벨은 FormField header가 아니라 input border 내부에 노출한다.
256
+ <span className="input-inline-label">{inlineLabel}</span>
257
+ ) : null}
242
258
  <input
243
259
  {...restProps}
244
260
  id={id ?? generatedId}
@@ -44,8 +44,9 @@ export default function InputBaseUtil({
44
44
  : state === "error"
45
45
  ? (error ?? baseStatusIcon)
46
46
  : null;
47
- // table priority는 상태를 border로만 표현하므로 status icon을 렌더하지 않는다.
48
- const resolvedStatusIcon = priority === "table" ? null : statusIcon;
47
+ // 변경: table/tertiary priority는 상태 아이콘을 렌더하지 않는다.
48
+ const resolvedStatusIcon =
49
+ priority === "table" || priority === "tertiary" ? null : statusIcon;
49
50
  const clearIconNode = clear ?? InputStatusIcon.reset;
50
51
  const showClearIcon = Boolean(
51
52
  clearIconNode &&
@@ -201,6 +201,12 @@
201
201
  border-width: var(--input-border-width-default);
202
202
  background-color: var(--input-surface-disabled-color);
203
203
  }
204
+
205
+ &[data-state="error"][data-readonly="true"] {
206
+ // 변경: calendar trigger처럼 readOnly input에서도 error state를 우선 노출한다.
207
+ border-color: var(--input-border-error-color);
208
+ border-width: var(--input-border-width-emphasis);
209
+ }
204
210
  }
205
211
  }
206
212
 
@@ -51,7 +51,8 @@
51
51
  --input-text-disabled-color: var(--color-label-disabled);
52
52
  --input-placeholder-color: var(--color-label-alternative);
53
53
  --input-placeholder-disabled-color: var(--color-label-disabled);
54
- --input-placeholder-readonly-color: var(--input-placeholder-disabled-color);
54
+ /* 변경: readonly 입력은 placeholder를 숨겨 value 텍스트 대비를 고정한다. */
55
+ --input-placeholder-readonly-color: transparent;
55
56
  --input-font-size: var(--font-body-medium-size);
56
57
  --input-line-height: var(--font-body-medium-line-height);
57
58
  --input-font-weight: 400;
@@ -7,7 +7,7 @@ import type {
7
7
  CalendarOnChange,
8
8
  CalendarValue,
9
9
  } from "../../calendar";
10
- import type { InputPriority } from "./foundation";
10
+ import type { InputPriority, InputState } from "./foundation";
11
11
 
12
12
  /**
13
13
  * Calendar trigger(Input) 영역에서 필요한 속성 묶음.
@@ -17,8 +17,10 @@ import type { InputPriority } from "./foundation";
17
17
  * @property {UseFormRegisterReturn} [register] react-hook-form register 결과
18
18
  * @property {(event: MouseEvent<Element>) => void} [onClick] trigger 클릭 핸들러
19
19
  * @property {boolean} [disabled] disabled 여부
20
+ * @property {boolean} [readOnly] readOnly 여부
20
21
  * @property {string} [placeholder] trigger 내부 placeholder
21
22
  * @property {InputPriority} [priority] trigger input priority
23
+ * @property {InputState} [state] trigger input state
22
24
  */
23
25
  export interface InputCalendarTriggerProps {
24
26
  /**
@@ -41,6 +43,10 @@ export interface InputCalendarTriggerProps {
41
43
  * disabled 여부
42
44
  */
43
45
  disabled?: boolean;
46
+ /**
47
+ * readOnly 여부
48
+ */
49
+ readOnly?: boolean;
44
50
  /**
45
51
  * trigger 내부 placeholder
46
52
  */
@@ -50,6 +56,10 @@ export interface InputCalendarTriggerProps {
50
56
  * - table일 때 Trigger는 left icon 배치를 사용한다.
51
57
  */
52
58
  priority?: InputPriority;
59
+ /**
60
+ * trigger input state
61
+ */
62
+ state?: InputState;
53
63
  }
54
64
 
55
65
  /**
@@ -75,8 +85,10 @@ export interface InputCalendarTriggerViewProps extends InputCalendarTriggerProps
75
85
  * @property {string} [displayValue] 트리거 표시 문자열
76
86
  * @property {string} [placeholder] 트리거 placeholder
77
87
  * @property {boolean} [disabled] disabled 여부
88
+ * @property {boolean} [readOnly] readOnly 여부
78
89
  * @property {(event: MouseEvent<Element>) => void} [onClick] 트리거 클릭 핸들러
79
90
  * @property {InputPriority} [priority] trigger input priority
91
+ * @property {InputState} [state] trigger input state
80
92
  */
81
93
  export interface InputCalendarTriggerRenderProps {
82
94
  /**
@@ -99,6 +111,10 @@ export interface InputCalendarTriggerRenderProps {
99
111
  * disabled 여부
100
112
  */
101
113
  disabled?: boolean;
114
+ /**
115
+ * readOnly 여부
116
+ */
117
+ readOnly?: boolean;
102
118
  /**
103
119
  * 트리거 클릭 핸들러
104
120
  */
@@ -108,6 +124,10 @@ export interface InputCalendarTriggerRenderProps {
108
124
  * - table일 때 Trigger는 left icon 배치를 사용한다.
109
125
  */
110
126
  priority?: InputPriority;
127
+ /**
128
+ * trigger input state
129
+ */
130
+ state?: InputState;
111
131
  }
112
132
 
113
133
  /**
@@ -122,6 +142,7 @@ export interface InputCalendarTriggerRenderProps {
122
142
  * @property {boolean} [disabled] disabled 여부
123
143
  * @property {CalendarDatePickerProps} [datePickerProps] Mantine DatePicker 직접 옵션
124
144
  * @property {InputPriority} [priority] trigger input priority
145
+ * @property {InputState} [state] trigger input state
125
146
  * @property {ReactNode} [header] 커스텀 header 콘텐츠
126
147
  * @property {ReactNode} [footer] 커스텀 footer 콘텐츠
127
148
  * @property {unknown} [timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
@@ -93,6 +93,7 @@ export interface InputIcon {
93
93
  * @property {InputSize} [size] 스타일 사이즈 타입
94
94
  * @property {InputState} [state] input 상태
95
95
  * @property {boolean} [block] width: 100% 여부
96
+ * @property {ReactNode} [inlineLabel] tertiary 내부 라벨
96
97
  * @property {string} [inputClassName]
97
98
  * @property {string} [boxClassName]
98
99
  * @property {UseFormRegisterReturn} [register]
@@ -121,6 +122,10 @@ export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
121
122
  * true면 width:100%
122
123
  */
123
124
  block?: boolean;
125
+ /**
126
+ * tertiary priority에서 border 내부 상단에 배치할 라벨 텍스트/노드
127
+ */
128
+ inlineLabel?: ReactNode;
124
129
  /**
125
130
  * 실제 `<input>` className
126
131
  */
@@ -272,15 +272,30 @@
272
272
  color: var(--select-table-color-placeholder);
273
273
  }
274
274
 
275
- .select-button[data-state="disabled"] &,
275
+ // 변경: disabled/readonly placeholder 색상을 상태 토큰으로 분리한다.
276
+ .select-button[data-state="disabled"] & {
277
+ color: var(--select-primary-color-placeholder-disabled);
278
+ }
279
+
276
280
  .select-button[data-readonly="true"] & {
277
- color: var(--select-primary-color-text-disabled);
281
+ color: var(--select-primary-color-placeholder-readonly);
278
282
  }
279
283
 
280
- .select-button[data-priority="secondary"][data-state="disabled"] &,
281
- .select-button[data-priority="secondary"][data-readonly="true"] & {
284
+ .select-button[data-priority="secondary"][data-state="disabled"] & {
282
285
  color: var(--select-secondary-color-placeholder-disabled);
283
286
  }
287
+
288
+ .select-button[data-priority="secondary"][data-readonly="true"] & {
289
+ color: var(--select-secondary-color-placeholder-readonly);
290
+ }
291
+
292
+ .select-button[data-priority="table"][data-state="disabled"] & {
293
+ color: var(--select-table-color-placeholder-disabled);
294
+ }
295
+
296
+ .select-button[data-priority="table"][data-readonly="true"] & {
297
+ color: var(--select-table-color-placeholder-readonly);
298
+ }
284
299
  }
285
300
 
286
301
  .select-button[data-size="small"] .select-label {
@@ -42,6 +42,12 @@
42
42
  --select-table-color-text: var(--input-text-color);
43
43
  --select-table-color-text-readonly: var(--select-primary-color-text-readonly);
44
44
  --select-table-color-placeholder: var(--input-placeholder-color);
45
+ --select-table-color-placeholder-disabled: var(
46
+ --select-primary-color-placeholder-disabled
47
+ );
48
+ --select-table-color-placeholder-readonly: var(
49
+ --select-primary-color-placeholder-readonly
50
+ );
45
51
  --select-table-icon-color-default: var(--select-icon-color-default);
46
52
  --select-table-icon-color-focused: var(--select-icon-color-focused);
47
53
  --select-table-icon-color-disabled: var(--select-icon-color-disabled);
@@ -78,6 +84,11 @@
78
84
  --select-icon-size-large: 2rem;
79
85
 
80
86
  --select-primary-color-placeholder: var(--input-placeholder-color);
87
+ /* 변경: placeholder disabled/readonly 토큰을 분리해 상태별 제어 지점을 고정한다. */
88
+ --select-primary-color-placeholder-disabled: var(--color-label-disabled);
89
+ --select-primary-color-placeholder-readonly: var(
90
+ --select-primary-color-placeholder-disabled
91
+ );
81
92
  --select-primary-color-surface: var(--input-surface-color);
82
93
  --select-primary-color-text: var(--color-label-alternative);
83
94
  --select-primary-color-text-focused: var(--color-label-strong);
@@ -1,3 +1,4 @@
1
+ import clsx from "clsx";
1
2
  import { forwardRef } from "react";
2
3
  import type { TableContainerProps } from "../types";
3
4
  import TableBody from "./foundation/Body";
@@ -11,8 +12,14 @@ import TableRow from "./foundation/Row";
11
12
  * Table Preset; 기본 Container 조합 컴포넌트
12
13
  * @component
13
14
  * @param {TableContainerProps} props
15
+ * @desc
16
+ * - columns 기반 colgroup/head 렌더링을 preset으로 제공한다.
17
+ * - `scrollable`이 true면 optional scroll wrapper를 추가해 소비처에서 스크롤 책임을 분리할 수 있다.
14
18
  * @param {TableColumnData[]} [props.columns] colgroup/head 자동 렌더링 column 목록
15
19
  * @param {boolean} [props.isCustomBody=false] true면 body wrapper 없이 children 직접 렌더링
20
+ * @param {boolean} [props.scrollable=false] true면 외부 스크롤 래퍼를 추가한다.
21
+ * @param {"x" | "y" | "both"} [props.scrollAxis="x"] scrollable일 때 스크롤 축
22
+ * @param {string} [props.scrollClassName] 스크롤 래퍼 className
16
23
  * @param {React.ReactNode} [props.footer] footer 노드. `<Table.Foot />`를 직접 전달하는 방식을 권장
17
24
  * @param {React.ReactNode} [props.children] table body 콘텐츠
18
25
  * @example
@@ -32,14 +39,32 @@ import TableRow from "./foundation/Row";
32
39
  * </Table.Container>
33
40
  */
34
41
  const TableContainer = forwardRef<HTMLTableElement, TableContainerProps>(
35
- ({ columns, isCustomBody = false, footer, children, ...tableProps }, ref) => {
42
+ (
43
+ {
44
+ columns,
45
+ isCustomBody = false,
46
+ scrollable = false,
47
+ scrollAxis = "x",
48
+ scrollClassName,
49
+ footer,
50
+ children,
51
+ ...tableProps
52
+ },
53
+ ref,
54
+ ) => {
36
55
  // 변경: optional columns를 안전하게 순회하기 위한 기본 배열을 고정한다.
37
56
  const resolvedColumns = columns ?? [];
38
57
  // 변경: columns 존재 여부만 단일 플래그로 관리해 렌더 분기 가독성을 유지한다.
39
58
  const hasColumns = resolvedColumns.length > 0;
59
+ // 변경: footer prop 주입 여부를 table data-attr로 노출해 grid radius 규칙 분기에 사용한다.
60
+ const hasFooter = typeof footer !== "undefined";
40
61
 
41
- return (
42
- <TableRoot {...tableProps} ref={ref}>
62
+ const tableNode = (
63
+ <TableRoot
64
+ {...tableProps}
65
+ ref={ref}
66
+ data-has-footer={hasFooter ? "true" : undefined}
67
+ >
43
68
  {hasColumns && (
44
69
  <colgroup>
45
70
  {resolvedColumns.map(({ key, width, dataKey, className }) => (
@@ -84,6 +109,19 @@ const TableContainer = forwardRef<HTMLTableElement, TableContainerProps>(
84
109
  {footer}
85
110
  </TableRoot>
86
111
  );
112
+
113
+ if (!scrollable) {
114
+ return tableNode;
115
+ }
116
+
117
+ return (
118
+ <div
119
+ className={clsx("table-scroll-wrapper", scrollClassName)}
120
+ data-scroll-axis={scrollAxis}
121
+ >
122
+ {tableNode}
123
+ </div>
124
+ );
87
125
  },
88
126
  );
89
127
 
@@ -6,8 +6,15 @@ import type { TableThProps } from "../../types";
6
6
  * Table Foundation; Th 마크업 컴포넌트
7
7
  * @component
8
8
  * @param {TableThProps} props
9
+ * @desc
10
+ * - sticky/레이어 우선순위는 `styles/foundation.scss`에서 section/column 기준으로 제어한다.
9
11
  * @param {string} [props.className] th className
12
+ * @param {React.CSSProperties} [props.style] th inline style
10
13
  * @param {React.ReactNode} [props.children] 셀 콘텐츠
14
+ * @example
15
+ * <Table.Th scope="row">
16
+ * <Table.Cell section="body">항목</Table.Cell>
17
+ * </Table.Th>
11
18
  */
12
19
  const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
13
20
  ({ className, children, scope, style, ...cellProps }, ref) => {
@@ -18,8 +25,8 @@ const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
18
25
  className={clsx("table-native-cell", "table-th", className)}
19
26
  // th 기본 scope는 col로 유지한다.
20
27
  scope={scope ?? "col"}
21
- // 변경: th/td 레벨 text-align은 기본 left 고정으로 유지한다.
22
- style={{ ...style, textAlign: "left" }}
28
+ // 변경: sticky/left/z-index는 CSS 규칙으로 위임해 인라인 우선순위 충돌을 방지한다.
29
+ style={style}
23
30
  >
24
31
  {/* 변경: 태그 셀렉터 의존을 피하기 위해 className 기반 텍스트 노드를 사용한다. */}
25
32
  {typeof children === "string" || typeof children === "number" ? (
@@ -49,19 +49,26 @@
49
49
  font-weight: 600;
50
50
  }
51
51
 
52
+ // 변경: line(list) 타입에서 rowspan 셀은 기본적으로 우측 경계선을 표시한다.
53
+ .table.table-container[data-layout="line"] .table-native-cell[rowspan] {
54
+ border-right: 1px solid var(--table-border-color);
55
+ }
56
+
52
57
  // 변경: grid 타입은 모든 셀 border와 헤더 배경을 기본 제공한다.
53
58
  .table.table-container[data-layout="grid"] .table-native-cell.table-th,
54
59
  .table.table-container[data-layout="grid"] .table-native-cell.table-td {
55
60
  // table cell 내부 field full-bleed 계산을 위해 padding 값을 변수로 노출한다.
56
61
  --table-cell-padding-inline: var(--table-grid-cell-padding-inline);
57
62
  --table-cell-padding-block: var(--table-grid-cell-padding-block);
58
- border: 1px solid var(--table-border-color);
59
63
  height: var(--table-grid-cell-height);
60
64
  }
61
65
 
62
66
  .table.table-container[data-layout="grid"] {
63
67
  border-radius: var(--table-grid-border-radius);
64
- overflow: hidden;
68
+ // 변경: Tooltip/Popover/Dropdown clipping을 막기 위해 grid container overflow 제거한다.
69
+ overflow: visible;
70
+ // 변경: sticky cell layering 기준점을 분리해 z-index 충돌을 줄인다.
71
+ isolation: isolate;
65
72
  }
66
73
 
67
74
  .table.table-container[data-layout="grid"]
@@ -77,6 +84,13 @@
77
84
  background-color: var(--table-grid-row-header-background-color);
78
85
  }
79
86
 
87
+ // 변경: grid foot의 th도 body row-header와 동일한 배경으로 유지해 sticky 겹침 시 시각 이탈을 방지한다.
88
+ .table.table-container[data-layout="grid"]
89
+ .table-foot
90
+ .table-native-cell.table-th {
91
+ background-color: var(--table-grid-row-header-background-color);
92
+ }
93
+
80
94
  // 변경: grid 타입 highlighted row는 row-header(th)보다 우선해 th/td 모두 blue tint를 적용한다.
81
95
  .table.table-container[data-layout="grid"]
82
96
  .table-row[data-highlighted="true"]
@@ -87,6 +101,185 @@
87
101
  background-color: var(--table-grid-row-highlight-background-color);
88
102
  }
89
103
 
104
+ // 변경: 인라인 스타일 대신 각 row의 첫 번째 th만 sticky로 고정한다.
105
+ .table.table-container .table-row > .table-native-cell.table-th:first-child {
106
+ position: sticky;
107
+ left: 0;
108
+ }
109
+
110
+ // 변경: sticky된 첫 번째 th가 td 위에서 안정적으로 보이도록 섹션별 z-index를 50단위로 분리한다.
111
+ .table.table-container
112
+ .table-head
113
+ .table-row
114
+ > .table-native-cell.table-th:first-child {
115
+ z-index: 150;
116
+ }
117
+
118
+ .table.table-container
119
+ .table-body
120
+ .table-row
121
+ > .table-native-cell.table-th:first-child {
122
+ z-index: 100;
123
+ }
124
+
125
+ .table.table-container
126
+ .table-foot
127
+ .table-row
128
+ > .table-native-cell.table-th:first-child {
129
+ z-index: 50;
130
+ }
131
+
132
+ // 변경: scroll 시 index 0 컬럼의 첫 header th가 항상 최상단에 보이도록 z-index를 상향한다.
133
+ .table.table-container
134
+ .table-head
135
+ .table-row:first-child
136
+ > .table-native-cell.table-th:first-child {
137
+ z-index: 200;
138
+ }
139
+
140
+ // 변경: sticky th와 겹치는 td 텍스트가 앞서 보이지 않도록 td를 기본 레이어로 고정한다.
141
+ .table.table-container .table-native-cell.table-td {
142
+ position: relative;
143
+ z-index: 0;
144
+ }
145
+
146
+ // 변경: grid 코너 라운드는 cell(th/td) 자체에 직접 적용해 sticky/scroll 상황에서도 모서리 형상을 유지한다.
147
+ .table.table-container[data-layout="grid"]
148
+ .table-head
149
+ .table-row:first-child
150
+ > .table-native-cell:first-child {
151
+ border-top-left-radius: var(--table-grid-border-radius);
152
+ }
153
+
154
+ .table.table-container[data-layout="grid"]
155
+ .table-head
156
+ .table-row:first-child
157
+ > .table-native-cell:last-child {
158
+ border-top-right-radius: var(--table-grid-border-radius);
159
+ }
160
+
161
+ .table.table-container[data-layout="grid"]
162
+ .table-foot
163
+ .table-row:last-child
164
+ > .table-native-cell:first-child,
165
+ .table.table-container[data-layout="grid"]:not([data-has-footer="true"])
166
+ .table-body:last-of-type
167
+ .table-row:last-child
168
+ > .table-native-cell:first-child {
169
+ border-bottom-left-radius: var(--table-grid-border-radius);
170
+ }
171
+
172
+ .table.table-container[data-layout="grid"]
173
+ .table-foot
174
+ .table-row:last-child
175
+ > .table-native-cell:last-child,
176
+ .table.table-container[data-layout="grid"]:not([data-has-footer="true"])
177
+ .table-body:last-of-type
178
+ .table-row:last-child
179
+ > .table-native-cell:last-child {
180
+ border-bottom-right-radius: var(--table-grid-border-radius);
181
+ }
182
+
183
+ // 변경: grid border를 native cell이 아닌 cell-content에 그려 sticky 겹침/코너 clipping을 안정화한다.
184
+ .table.table-container[data-layout="grid"] .table-cell-content {
185
+ border-right: 1px solid var(--table-border-color);
186
+ border-bottom: 1px solid var(--table-border-color);
187
+ }
188
+
189
+ // 변경: border-top은 thead 내부 첫 row의 cell에만 적용한다.
190
+ .table.table-container[data-layout="grid"]
191
+ .table-head
192
+ .table-row:first-child
193
+ > .table-native-cell
194
+ > .table-cell-content {
195
+ border-top: 1px solid var(--table-border-color);
196
+ }
197
+
198
+ .table.table-container[data-layout="grid"]
199
+ .table-row
200
+ > .table-native-cell:first-child
201
+ > .table-cell-content {
202
+ border-left: 1px solid var(--table-border-color);
203
+ }
204
+
205
+ // 변경: grid 병합(rowspan) 셀은 우측 border를 제거하고, 인접 다음 셀의 좌측 border로 경계를 표현한다.
206
+ .table.table-container[data-layout="grid"]
207
+ .table-row
208
+ > .table-native-cell[rowspan]
209
+ > .table-cell-content {
210
+ border-right: 0;
211
+ }
212
+
213
+ .table.table-container[data-layout="grid"]
214
+ .table-row
215
+ > .table-native-cell[rowspan]
216
+ + .table-native-cell
217
+ > .table-cell-content {
218
+ border-left: 1px solid var(--table-border-color);
219
+ }
220
+
221
+ // 변경: 요청사항(and)에 맞춰 코너 셀의 th/td와 내부 cell-content 모두 동일 반경을 공유한다.
222
+
223
+ .table.table-container[data-layout="grid"]
224
+ .table-row:first-child
225
+ > .table-native-cell:first-child
226
+ .table-cell-content {
227
+ border-top-left-radius: inherit;
228
+ }
229
+
230
+ .table.table-container[data-layout="grid"]
231
+ .table-row:first-child
232
+ > .table-native-cell:last-child
233
+ .table-cell-content {
234
+ border-top-right-radius: inherit;
235
+ }
236
+
237
+ .table.table-container[data-layout="grid"]
238
+ .table-foot
239
+ .table-row:last-child
240
+ > .table-native-cell:first-child
241
+ .table-cell-content,
242
+ .table.table-container[data-layout="grid"]:not([data-has-footer="true"])
243
+ .table-body:last-of-type
244
+ .table-row:last-child
245
+ > .table-native-cell:first-child
246
+ .table-cell-content {
247
+ border-bottom-left-radius: inherit;
248
+ }
249
+
250
+ .table.table-container[data-layout="grid"]
251
+ .table-foot
252
+ .table-row:last-child
253
+ > .table-native-cell:last-child
254
+ .table-cell-content,
255
+ .table.table-container[data-layout="grid"]:not([data-has-footer="true"])
256
+ .table-body:last-of-type
257
+ .table-row:last-child
258
+ > .table-native-cell:last-child
259
+ .table-cell-content {
260
+ border-bottom-right-radius: inherit;
261
+ }
262
+
263
+ // 변경: optional scroll 래퍼를 통해 필요한 화면에서만 스크롤 레이어를 분리한다.
264
+ .table-scroll-wrapper {
265
+ width: 100%;
266
+ border-radius: var(--table-grid-border-radius);
267
+ }
268
+
269
+ .table-scroll-wrapper[data-scroll-axis="x"] {
270
+ overflow-x: auto;
271
+ overflow-y: visible;
272
+ }
273
+
274
+ .table-scroll-wrapper[data-scroll-axis="y"] {
275
+ overflow-x: visible;
276
+ overflow-y: auto;
277
+ }
278
+
279
+ .table-scroll-wrapper[data-scroll-axis="both"] {
280
+ overflow: auto;
281
+ }
282
+
90
283
  // 변경: tag selector 대신 className selector로 텍스트 스타일 적용 범위를 고정한다.
91
284
  .table-native-cell.table-th .table-native-cell-text {
92
285
  color: var(--table-th-text-color);
@@ -333,6 +526,15 @@
333
526
  line-height: var(--table-td-text-line-height);
334
527
  }
335
528
 
529
+ // 변경: footer(table-foot) 내부 셀 콘텐츠 타이포를 body와 동일한 td 스케일로 고정한다.
530
+ .table.table-container .table-foot .table-cell-content,
531
+ .table.table-container .table-foot .table-cell-text {
532
+ color: var(--table-td-text-color);
533
+ font-size: var(--table-td-text-size);
534
+ font-weight: var(--table-td-text-weight);
535
+ line-height: var(--table-td-text-line-height);
536
+ }
537
+
336
538
  // 변경: Cell(body/foot) 컨텍스트에서는 table priority input/select 텍스트를 15px 축으로 강제한다.
337
539
  .table-cell.table-body-cell .radio-label-text,
338
540
  .table-cell.table-foot-cell .radio-label-text {
@@ -31,7 +31,13 @@ export const TABLE_CELL_ALIGN_ITEMS_OPTIONS = [
31
31
  export const TABLE_CELL_PADDING_OPTIONS = ["default", "none"] as const;
32
32
  export const TABLE_SECTIONS = ["head", "body", "foot"] as const;
33
33
  export const TABLE_LAYOUT_OPTIONS = ["line", "grid"] as const;
34
+ export const TABLE_SCROLL_AXES = ["x", "y", "both"] as const;
34
35
 
36
+ /**
37
+ * Table Types; 스크롤 축 타입
38
+ * @typedef {"x" | "y" | "both"} TableScrollAxis
39
+ */
40
+ export type TableScrollAxis = (typeof TABLE_SCROLL_AXES)[number];
35
41
  /**
36
42
  * Table Types; Cell 가로 정렬 값 타입
37
43
  * @typedef {"left" | "center" | "right" | "normal" | "start" | "end" | "flex-start" | "flex-end" | "space-between" | "space-around" | "space-evenly" | "stretch"} TableCellAlignX
@@ -126,6 +132,9 @@ export interface TableColumnData {
126
132
  * Table Types; Container props
127
133
  * @property {TableColumnData[]} [columns] colgroup/head 자동 렌더링용 column 데이터
128
134
  * @property {boolean} [isCustomBody] true면 body wrapper 없이 children을 직접 렌더링
135
+ * @property {boolean} [scrollable=false] true면 외부 스크롤 래퍼를 추가한다.
136
+ * @property {"x" | "y" | "both"} [scrollAxis="x"] scrollable일 때 스크롤 축
137
+ * @property {string} [scrollClassName] 스크롤 래퍼 className
129
138
  * @property {React.ReactNode} [footer] footer 노드
130
139
  * @property {React.ReactNode} [children] body 콘텐츠
131
140
  */
@@ -138,6 +147,18 @@ export interface TableContainerProps extends TableRootProps {
138
147
  * true면 body wrapper 없이 children을 직접 렌더링
139
148
  */
140
149
  isCustomBody?: boolean;
150
+ /**
151
+ * true면 외부 스크롤 래퍼를 추가한다.
152
+ */
153
+ scrollable?: boolean;
154
+ /**
155
+ * scrollable일 때 스크롤 축
156
+ */
157
+ scrollAxis?: TableScrollAxis;
158
+ /**
159
+ * 스크롤 래퍼 className
160
+ */
161
+ scrollClassName?: string;
141
162
  /**
142
163
  * footer 노드
143
164
  */
@@ -187,6 +208,7 @@ export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
187
208
  /**
188
209
  * Table Types; Th props
189
210
  * @property {string} [className] th className
211
+ * @property {React.CSSProperties} [style] sticky 기본값(position/left/z-index/textAlign) override style
190
212
  * @property {React.ReactNode} [children] header cell 콘텐츠
191
213
  */
192
214
  export interface TableThProps extends ComponentPropsWithoutRef<"th"> {}