@ncds/ui-admin 1.8.7 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cjs/src/components/data-display/table/Table.js +2 -0
  2. package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +6 -2
  3. package/dist/cjs/src/components/feedback-and-status/badge/Badge.js +19 -0
  4. package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +3 -1
  5. package/dist/cjs/src/components/forms-and-input/image-file-input/ImageFileInput.js +30 -25
  6. package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +3 -1
  7. package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +5 -4
  8. package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +1 -1
  9. package/dist/esm/src/components/data-display/table/Table.js +2 -0
  10. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +6 -2
  11. package/dist/esm/src/components/feedback-and-status/badge/Badge.js +19 -0
  12. package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +3 -1
  13. package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +30 -25
  14. package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +3 -1
  15. package/dist/esm/src/components/overlays/tooltip/Tooltip.js +5 -4
  16. package/dist/esm/src/components/select-dropdown/SelectDropdown.js +1 -1
  17. package/dist/temp/src/components/data-display/table/Table.js +2 -0
  18. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +3 -1
  19. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +7 -3
  20. package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +3 -2
  21. package/dist/temp/src/components/feedback-and-status/badge/Badge.js +10 -0
  22. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +4 -0
  23. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +3 -2
  24. package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +17 -14
  25. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +4 -0
  26. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +3 -2
  27. package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +2 -1
  28. package/dist/temp/src/components/overlays/tooltip/Tooltip.js +4 -4
  29. package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +1 -1
  30. package/dist/temp/src/components/select-dropdown/SelectDropdown.js +1 -1
  31. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +3 -1
  32. package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +3 -2
  33. package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +4 -0
  34. package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +4 -0
  35. package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +2 -1
  36. package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +1 -1
  37. package/dist/ui-admin/assets/styles/style.css +71 -4
  38. package/package.json +2 -2
@@ -281,8 +281,10 @@ const TableComponent = /*#__PURE__*/(0, _react.forwardRef)((_ref0, ref) => {
281
281
  handleThumbMouseDown
282
282
  } = (0, _useTableScrollbars.useTableVerticalScrollbar)({
283
283
  enabled: fixedScrollEnabled,
284
+ layoutKey: horizontalScroll,
284
285
  scrollContainerRef,
285
286
  scrollAreaRef,
287
+ scrollbarRef,
286
288
  thumbRef
287
289
  });
288
290
  const {
@@ -72,10 +72,13 @@ const startDrag = (e, options) => {
72
72
  const useTableVerticalScrollbar = _ref => {
73
73
  let {
74
74
  enabled,
75
+ layoutKey,
75
76
  scrollContainerRef,
76
77
  scrollAreaRef,
78
+ scrollbarRef,
77
79
  thumbRef
78
80
  } = _ref;
81
+ // biome-ignore lint/correctness/useExhaustiveDependencies: layoutKey는 직접 참조하지 않지만 horizontalScroll 토글로 스크롤바 DOM이 리마운트될 때 effect를 재바인딩하기 위한 트리거로 의도적으로 포함한다.
79
82
  (0, _react.useEffect)(() => {
80
83
  if (!enabled) return;
81
84
  const scrollEl = scrollContainerRef.current;
@@ -91,7 +94,7 @@ const useTableVerticalScrollbar = _ref => {
91
94
  thumbEl.style.height = '0';
92
95
  return;
93
96
  }
94
- const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
97
+ const trackHeight = scrollbarRef.current?.clientHeight || (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
95
98
  const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, clientHeight / scrollHeight * trackHeight);
96
99
  const thumbTop = scrollTop / (scrollHeight - clientHeight) * (trackHeight - thumbHeight);
97
100
  thumbEl.style.height = `${thumbHeight}px`;
@@ -103,12 +106,13 @@ const useTableVerticalScrollbar = _ref => {
103
106
  const observer = new ResizeObserver(update);
104
107
  observer.observe(scrollEl);
105
108
  if (scrollAreaRef.current) observer.observe(scrollAreaRef.current);
109
+ if (scrollbarRef.current) observer.observe(scrollbarRef.current);
106
110
  update();
107
111
  return () => {
108
112
  scrollEl.removeEventListener('scroll', update);
109
113
  observer.disconnect();
110
114
  };
111
- }, [enabled, scrollContainerRef, scrollAreaRef, thumbRef]);
115
+ }, [enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef]);
112
116
  const handleThumbMouseDown = e => {
113
117
  const scrollEl = scrollContainerRef.current;
114
118
  const thumbEl = thumbRef.current;
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.Badge = void 0;
7
7
  var _jsxRuntime = require("react/jsx-runtime");
8
+ var _uiAdminIcon = require("@ncds/ui-admin-icon");
8
9
  var _classnames = _interopRequireDefault(require("classnames"));
9
10
  var _utils = require("./utils");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -12,6 +13,11 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
12
13
  * 뱃지 컴포넌트의 아이콘은 디자인 시스템에서 12px 고정으로 정의되어 있습니다.
13
14
  */
14
15
  const BADGE_ICON_SIZE = 12;
16
+ /** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
17
+ const NEW_BADGE_ICON_SIZE = {
18
+ sm: 12,
19
+ md: 16
20
+ };
15
21
  const Badge = _ref => {
16
22
  let {
17
23
  label,
@@ -22,6 +28,19 @@ const Badge = _ref => {
22
28
  trailingIcon,
23
29
  size = 'xs'
24
30
  } = _ref;
31
+ // new-badge: 신규 콘텐츠 'N' 마크 전용 타입. label·color·leadingIcon·trailingIcon은 무시되고
32
+ // 색은 --pink-600 으로 고정된다. (디자이너 명세: Icon/FeaturedIcon/Badge로 대체 불가한 전용 표시)
33
+ if (type === 'new-badge') {
34
+ const newBadgeSize = size === 'md' ? 'md' : 'sm';
35
+ const iconSize = NEW_BADGE_ICON_SIZE[newBadgeSize];
36
+ return (0, _jsxRuntime.jsx)("span", {
37
+ className: (0, _classnames.default)('ncua-badge', 'ncua-badge--new-badge', `ncua-badge--new-badge-${newBadgeSize}`, className),
38
+ children: (0, _jsxRuntime.jsx)(_uiAdminIcon.New, {
39
+ width: iconSize,
40
+ height: iconSize
41
+ })
42
+ });
43
+ }
25
44
  return (0, _jsxRuntime.jsxs)("span", {
26
45
  className: (0, _classnames.default)('ncua-badge', `ncua-badge--${type}`, `ncua-badge--${color}`, `ncua-badge--${size}`, className),
27
46
  children: [leadingIcon && (0, _utils.sideSlotRender)({
@@ -50,6 +50,7 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
50
50
  showFooterButtons = false,
51
51
  maxSelection,
52
52
  onEdit,
53
+ onComplete,
53
54
  ...props
54
55
  } = _ref;
55
56
  const internalRef = (0, _react.useRef)(null);
@@ -183,12 +184,13 @@ const ComboBox = exports.ComboBox = /*#__PURE__*/(0, _react.forwardRef)((_ref, r
183
184
  const handleEdit = () => {
184
185
  onEdit?.();
185
186
  };
186
- const handleComplete = () => {
187
+ const handleComplete = completedValue => {
187
188
  if (!multiple) return;
188
189
  const tags = getSelectedTagsData();
189
190
  setSelectedTags(tags);
190
191
  setInputValue('');
191
192
  closeDropdown();
193
+ onComplete?.(completedValue);
192
194
  };
193
195
  const handleRemoveTag = tagId => {
194
196
  if (!onChange) return;
@@ -14,6 +14,20 @@ var _shared = require("../../shared");
14
14
  var _FileInput = require("../file-input/FileInput");
15
15
  var _ImagePreview = require("./components/ImagePreview");
16
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
+ const toInvalidFile = (file, errorType) => ({
18
+ name: file.name,
19
+ size: file.size,
20
+ type: file.type,
21
+ lastModified: file.lastModified,
22
+ webkitRelativePath: file.webkitRelativePath,
23
+ arrayBuffer: () => file.arrayBuffer(),
24
+ stream: () => file.stream(),
25
+ text: () => file.text(),
26
+ slice: function () {
27
+ return file.slice(...arguments);
28
+ },
29
+ errorType
30
+ });
17
31
  const ImageFileInput = exports.ImageFileInput = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
18
32
  let {
19
33
  size = 'sm',
@@ -81,25 +95,16 @@ const ImageFileInput = exports.ImageFileInput = /*#__PURE__*/(0, _react.forwardR
81
95
  const invalidFiles = [];
82
96
  for (const file of fileList) {
83
97
  if (files.some(f => f.name === file.name && f.size === file.size)) {
84
- invalidFiles.push({
85
- ...file,
86
- errorType: _FileInput.FileInputErrorType.ALREADY_UPLOADED
87
- });
98
+ invalidFiles.push(toInvalidFile(file, _FileInput.FileInputErrorType.ALREADY_UPLOADED));
88
99
  continue;
89
100
  }
90
101
  if (!!maxFileSize && file.size > maxFileSize) {
91
- invalidFiles.push({
92
- ...file,
93
- errorType: _FileInput.FileInputErrorType.EXCEED_MAX_FILE_SIZE
94
- });
102
+ invalidFiles.push(toInvalidFile(file, _FileInput.FileInputErrorType.EXCEED_MAX_FILE_SIZE));
95
103
  continue;
96
104
  }
97
105
  // Skip max count check if maxFileCount is 1 (allow replacement)
98
106
  if (!!maxFileCount && maxFileCount !== 1 && files.length + validFiles.length >= maxFileCount) {
99
- invalidFiles.push({
100
- ...file,
101
- errorType: _FileInput.FileInputErrorType.EXCEED_MAX_FILE_COUNT
102
- });
107
+ invalidFiles.push(toInvalidFile(file, _FileInput.FileInputErrorType.EXCEED_MAX_FILE_COUNT));
103
108
  continue;
104
109
  }
105
110
  validFiles.push(file);
@@ -144,7 +149,7 @@ const ImageFileInput = exports.ImageFileInput = /*#__PURE__*/(0, _react.forwardR
144
149
  position: "bottom",
145
150
  tooltipType: "black",
146
151
  forceVisible: isButtonHovered && !disabled,
147
- disablePortal: true
152
+ panelClassName: (0, _classnames.default)('ncua-image-file-input__slot-tooltip', `ncua-image-file-input__slot-tooltip--${size}`)
148
153
  })]
149
154
  })]
150
155
  });
@@ -163,19 +168,19 @@ const ImageFileInput = exports.ImageFileInput = /*#__PURE__*/(0, _react.forwardR
163
168
  className: (0, _classnames.default)('ncua-image-file-input', `ncua-image-file-input--${size}`, {
164
169
  destructive: destructive
165
170
  }),
166
- children: [renderImagePreview(files), showFileInput && (0, _jsxRuntime.jsxs)("div", {
171
+ children: [renderImagePreview(files), (0, _jsxRuntime.jsx)("input", {
172
+ hidden: true,
173
+ ref: fileInputRef,
174
+ type: "file",
175
+ accept: accept,
176
+ multiple: multiple,
177
+ onChange: handleFileChange,
178
+ tabIndex: -1,
179
+ "aria-hidden": "true",
180
+ ...props
181
+ }), showFileInput && (0, _jsxRuntime.jsxs)("div", {
167
182
  className: (0, _classnames.default)('ncua-file-input', `ncua-file-input--${size}`),
168
- children: [(0, _jsxRuntime.jsx)("input", {
169
- hidden: true,
170
- ref: fileInputRef,
171
- type: "file",
172
- accept: accept,
173
- multiple: multiple,
174
- onChange: handleFileChange,
175
- tabIndex: -1,
176
- "aria-hidden": "true",
177
- ...props
178
- }), (0, _jsxRuntime.jsxs)("div", {
183
+ children: [(0, _jsxRuntime.jsxs)("div", {
179
184
  className: "ncua-file-input__input-container",
180
185
  children: [(0, _jsxRuntime.jsxs)("div", {
181
186
  className: "ncua-file-input__label",
@@ -89,6 +89,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
89
89
  register,
90
90
  onChange,
91
91
  onEdit,
92
+ onComplete,
92
93
  ...props
93
94
  } = _ref2;
94
95
  const internalRef = (0, _react.useRef)(null);
@@ -173,10 +174,11 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
173
174
  const handleEdit = () => {
174
175
  onEdit?.();
175
176
  };
176
- const handleComplete = () => {
177
+ const handleComplete = completedValue => {
177
178
  if (multiple) {
178
179
  const tags = getSelectedTagsData();
179
180
  setSelectedTags(tags);
181
+ onComplete?.(completedValue);
180
182
  }
181
183
  closeDropdown();
182
184
  };
@@ -128,7 +128,8 @@ const Tooltip = _ref => {
128
128
  className,
129
129
  zIndex,
130
130
  forceVisible,
131
- disablePortal = false
131
+ disablePortal = false,
132
+ panelClassName
132
133
  } = _ref;
133
134
  const iconSize = size === 'sm' ? ICON_SIZE_SM : ICON_SIZE_DEFAULT;
134
135
  const anchorRef = (0, _react.useRef)(null);
@@ -206,17 +207,17 @@ const Tooltip = _ref => {
206
207
  'ncua-tooltip--stroke': iconType === 'stroke',
207
208
  'ncua-tooltip--auto': position === 'auto'
208
209
  }, className), [size, type, hideArrow, iconType, position, className]);
209
- const panelClassName = (0, _react.useMemo)(() => (0, _classnames.default)({
210
+ const panelClasses = (0, _react.useMemo)(() => (0, _classnames.default)({
210
211
  'ncua-tooltip-panel': !disablePortal
211
212
  }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
212
213
  'ncua-tooltip__bg--visible': effectiveVisible,
213
214
  'ncua-tooltip__bg--force-hidden': isManuallyClose && !forceVisible
214
- }), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal]);
215
+ }, panelClassName), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal, panelClassName]);
215
216
  const panelStyle = buildPanelStyle(disablePortal, coords, effectiveVisible, zIndex);
216
217
  const portalTarget = mounted ? resolvePortalTarget() : null;
217
218
  const panel = (0, _jsxRuntime.jsxs)("span", {
218
219
  ref: panelRef,
219
- className: panelClassName,
220
+ className: panelClasses,
220
221
  style: panelStyle,
221
222
  children: [title && (0, _jsxRuntime.jsx)("span", {
222
223
  className: "ncua-tooltip__title",
@@ -111,7 +111,7 @@ const SelectDropdown = exports.SelectDropdown = /*#__PURE__*/(0, _react.forwardR
111
111
  label: "\uC120\uD0DD \uC644\uB8CC",
112
112
  hierarchy: "secondary",
113
113
  size: "xs",
114
- onClick: onComplete
114
+ onClick: () => onComplete?.(value ?? [])
115
115
  })]
116
116
  })]
117
117
  })
@@ -274,8 +274,10 @@ const TableComponent = /*#__PURE__*/forwardRef((_ref0, ref) => {
274
274
  handleThumbMouseDown
275
275
  } = useTableVerticalScrollbar({
276
276
  enabled: fixedScrollEnabled,
277
+ layoutKey: horizontalScroll,
277
278
  scrollContainerRef,
278
279
  scrollAreaRef,
280
+ scrollbarRef,
279
281
  thumbRef
280
282
  });
281
283
  const {
@@ -66,10 +66,13 @@ const startDrag = (e, options) => {
66
66
  export const useTableVerticalScrollbar = _ref => {
67
67
  let {
68
68
  enabled,
69
+ layoutKey,
69
70
  scrollContainerRef,
70
71
  scrollAreaRef,
72
+ scrollbarRef,
71
73
  thumbRef
72
74
  } = _ref;
75
+ // biome-ignore lint/correctness/useExhaustiveDependencies: layoutKey는 직접 참조하지 않지만 horizontalScroll 토글로 스크롤바 DOM이 리마운트될 때 effect를 재바인딩하기 위한 트리거로 의도적으로 포함한다.
73
76
  useEffect(() => {
74
77
  if (!enabled) return;
75
78
  const scrollEl = scrollContainerRef.current;
@@ -85,7 +88,7 @@ export const useTableVerticalScrollbar = _ref => {
85
88
  thumbEl.style.height = '0';
86
89
  return;
87
90
  }
88
- const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
91
+ const trackHeight = scrollbarRef.current?.clientHeight || (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
89
92
  const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, clientHeight / scrollHeight * trackHeight);
90
93
  const thumbTop = scrollTop / (scrollHeight - clientHeight) * (trackHeight - thumbHeight);
91
94
  thumbEl.style.height = `${thumbHeight}px`;
@@ -97,12 +100,13 @@ export const useTableVerticalScrollbar = _ref => {
97
100
  const observer = new ResizeObserver(update);
98
101
  observer.observe(scrollEl);
99
102
  if (scrollAreaRef.current) observer.observe(scrollAreaRef.current);
103
+ if (scrollbarRef.current) observer.observe(scrollbarRef.current);
100
104
  update();
101
105
  return () => {
102
106
  scrollEl.removeEventListener('scroll', update);
103
107
  observer.disconnect();
104
108
  };
105
- }, [enabled, scrollContainerRef, scrollAreaRef, thumbRef]);
109
+ }, [enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef]);
106
110
  const handleThumbMouseDown = e => {
107
111
  const scrollEl = scrollContainerRef.current;
108
112
  const thumbEl = thumbRef.current;
@@ -1,10 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { New } from '@ncds/ui-admin-icon';
2
3
  import classNames from 'classnames';
3
4
  import { sideSlotRender } from './utils';
4
5
  /**
5
6
  * 뱃지 컴포넌트의 아이콘은 디자인 시스템에서 12px 고정으로 정의되어 있습니다.
6
7
  */
7
8
  const BADGE_ICON_SIZE = 12;
9
+ /** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
10
+ const NEW_BADGE_ICON_SIZE = {
11
+ sm: 12,
12
+ md: 16
13
+ };
8
14
  const Badge = _ref => {
9
15
  let {
10
16
  label,
@@ -15,6 +21,19 @@ const Badge = _ref => {
15
21
  trailingIcon,
16
22
  size = 'xs'
17
23
  } = _ref;
24
+ // new-badge: 신규 콘텐츠 'N' 마크 전용 타입. label·color·leadingIcon·trailingIcon은 무시되고
25
+ // 색은 --pink-600 으로 고정된다. (디자이너 명세: Icon/FeaturedIcon/Badge로 대체 불가한 전용 표시)
26
+ if (type === 'new-badge') {
27
+ const newBadgeSize = size === 'md' ? 'md' : 'sm';
28
+ const iconSize = NEW_BADGE_ICON_SIZE[newBadgeSize];
29
+ return _jsx("span", {
30
+ className: classNames('ncua-badge', 'ncua-badge--new-badge', `ncua-badge--new-badge-${newBadgeSize}`, className),
31
+ children: _jsx(New, {
32
+ width: iconSize,
33
+ height: iconSize
34
+ })
35
+ });
36
+ }
18
37
  return _jsxs("span", {
19
38
  className: classNames('ncua-badge', `ncua-badge--${type}`, `ncua-badge--${color}`, `ncua-badge--${size}`, className),
20
39
  children: [leadingIcon && sideSlotRender({
@@ -43,6 +43,7 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
43
43
  showFooterButtons = false,
44
44
  maxSelection,
45
45
  onEdit,
46
+ onComplete,
46
47
  ...props
47
48
  } = _ref;
48
49
  const internalRef = useRef(null);
@@ -176,12 +177,13 @@ const ComboBox = /*#__PURE__*/forwardRef((_ref, ref) => {
176
177
  const handleEdit = () => {
177
178
  onEdit?.();
178
179
  };
179
- const handleComplete = () => {
180
+ const handleComplete = completedValue => {
180
181
  if (!multiple) return;
181
182
  const tags = getSelectedTagsData();
182
183
  setSelectedTags(tags);
183
184
  setInputValue('');
184
185
  closeDropdown();
186
+ onComplete?.(completedValue);
185
187
  };
186
188
  const handleRemoveTag = tagId => {
187
189
  if (!onChange) return;
@@ -7,6 +7,20 @@ import { Tooltip } from '../../overlays/tooltip';
7
7
  import { HintText, Label } from '../../shared';
8
8
  import { FileInputErrorType as ImageFileInputErrorType } from '../file-input/FileInput';
9
9
  import { ImagePreview } from './components/ImagePreview';
10
+ const toInvalidFile = (file, errorType) => ({
11
+ name: file.name,
12
+ size: file.size,
13
+ type: file.type,
14
+ lastModified: file.lastModified,
15
+ webkitRelativePath: file.webkitRelativePath,
16
+ arrayBuffer: () => file.arrayBuffer(),
17
+ stream: () => file.stream(),
18
+ text: () => file.text(),
19
+ slice: function () {
20
+ return file.slice(...arguments);
21
+ },
22
+ errorType
23
+ });
10
24
  export const ImageFileInput = /*#__PURE__*/forwardRef((_ref, ref) => {
11
25
  let {
12
26
  size = 'sm',
@@ -74,25 +88,16 @@ export const ImageFileInput = /*#__PURE__*/forwardRef((_ref, ref) => {
74
88
  const invalidFiles = [];
75
89
  for (const file of fileList) {
76
90
  if (files.some(f => f.name === file.name && f.size === file.size)) {
77
- invalidFiles.push({
78
- ...file,
79
- errorType: ImageFileInputErrorType.ALREADY_UPLOADED
80
- });
91
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.ALREADY_UPLOADED));
81
92
  continue;
82
93
  }
83
94
  if (!!maxFileSize && file.size > maxFileSize) {
84
- invalidFiles.push({
85
- ...file,
86
- errorType: ImageFileInputErrorType.EXCEED_MAX_FILE_SIZE
87
- });
95
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.EXCEED_MAX_FILE_SIZE));
88
96
  continue;
89
97
  }
90
98
  // Skip max count check if maxFileCount is 1 (allow replacement)
91
99
  if (!!maxFileCount && maxFileCount !== 1 && files.length + validFiles.length >= maxFileCount) {
92
- invalidFiles.push({
93
- ...file,
94
- errorType: ImageFileInputErrorType.EXCEED_MAX_FILE_COUNT
95
- });
100
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.EXCEED_MAX_FILE_COUNT));
96
101
  continue;
97
102
  }
98
103
  validFiles.push(file);
@@ -137,7 +142,7 @@ export const ImageFileInput = /*#__PURE__*/forwardRef((_ref, ref) => {
137
142
  position: "bottom",
138
143
  tooltipType: "black",
139
144
  forceVisible: isButtonHovered && !disabled,
140
- disablePortal: true
145
+ panelClassName: classNames('ncua-image-file-input__slot-tooltip', `ncua-image-file-input__slot-tooltip--${size}`)
141
146
  })]
142
147
  })]
143
148
  });
@@ -156,19 +161,19 @@ export const ImageFileInput = /*#__PURE__*/forwardRef((_ref, ref) => {
156
161
  className: classNames('ncua-image-file-input', `ncua-image-file-input--${size}`, {
157
162
  destructive: destructive
158
163
  }),
159
- children: [renderImagePreview(files), showFileInput && _jsxs("div", {
164
+ children: [renderImagePreview(files), _jsx("input", {
165
+ hidden: true,
166
+ ref: fileInputRef,
167
+ type: "file",
168
+ accept: accept,
169
+ multiple: multiple,
170
+ onChange: handleFileChange,
171
+ tabIndex: -1,
172
+ "aria-hidden": "true",
173
+ ...props
174
+ }), showFileInput && _jsxs("div", {
160
175
  className: classNames('ncua-file-input', `ncua-file-input--${size}`),
161
- children: [_jsx("input", {
162
- hidden: true,
163
- ref: fileInputRef,
164
- type: "file",
165
- accept: accept,
166
- multiple: multiple,
167
- onChange: handleFileChange,
168
- tabIndex: -1,
169
- "aria-hidden": "true",
170
- ...props
171
- }), _jsxs("div", {
176
+ children: [_jsxs("div", {
172
177
  className: "ncua-file-input__input-container",
173
178
  children: [_jsxs("div", {
174
179
  className: "ncua-file-input__label",
@@ -82,6 +82,7 @@ const SelectBox = /*#__PURE__*/forwardRef((_ref2, ref
82
82
  register,
83
83
  onChange,
84
84
  onEdit,
85
+ onComplete,
85
86
  ...props
86
87
  } = _ref2;
87
88
  const internalRef = useRef(null);
@@ -166,10 +167,11 @@ const SelectBox = /*#__PURE__*/forwardRef((_ref2, ref
166
167
  const handleEdit = () => {
167
168
  onEdit?.();
168
169
  };
169
- const handleComplete = () => {
170
+ const handleComplete = completedValue => {
170
171
  if (multiple) {
171
172
  const tags = getSelectedTagsData();
172
173
  setSelectedTags(tags);
174
+ onComplete?.(completedValue);
173
175
  }
174
176
  closeDropdown();
175
177
  };
@@ -121,7 +121,8 @@ export const Tooltip = _ref => {
121
121
  className,
122
122
  zIndex,
123
123
  forceVisible,
124
- disablePortal = false
124
+ disablePortal = false,
125
+ panelClassName
125
126
  } = _ref;
126
127
  const iconSize = size === 'sm' ? ICON_SIZE_SM : ICON_SIZE_DEFAULT;
127
128
  const anchorRef = useRef(null);
@@ -199,17 +200,17 @@ export const Tooltip = _ref => {
199
200
  'ncua-tooltip--stroke': iconType === 'stroke',
200
201
  'ncua-tooltip--auto': position === 'auto'
201
202
  }, className), [size, type, hideArrow, iconType, position, className]);
202
- const panelClassName = useMemo(() => classNames({
203
+ const panelClasses = useMemo(() => classNames({
203
204
  'ncua-tooltip-panel': !disablePortal
204
205
  }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
205
206
  'ncua-tooltip__bg--visible': effectiveVisible,
206
207
  'ncua-tooltip__bg--force-hidden': isManuallyClose && !forceVisible
207
- }), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal]);
208
+ }, panelClassName), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal, panelClassName]);
208
209
  const panelStyle = buildPanelStyle(disablePortal, coords, effectiveVisible, zIndex);
209
210
  const portalTarget = mounted ? resolvePortalTarget() : null;
210
211
  const panel = _jsxs("span", {
211
212
  ref: panelRef,
212
- className: panelClassName,
213
+ className: panelClasses,
213
214
  style: panelStyle,
214
215
  children: [title && _jsx("span", {
215
216
  className: "ncua-tooltip__title",
@@ -104,7 +104,7 @@ const SelectDropdown = /*#__PURE__*/forwardRef((_ref, ref) => {
104
104
  label: "\uC120\uD0DD \uC644\uB8CC",
105
105
  hierarchy: "secondary",
106
106
  size: "xs",
107
- onClick: onComplete
107
+ onClick: () => onComplete?.(value ?? [])
108
108
  })]
109
109
  })]
110
110
  })
@@ -136,8 +136,10 @@ const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, m
136
136
  const fixedScrollEnabled = !!(fixedHeader && maxHeight);
137
137
  const { handleThumbMouseDown } = useTableVerticalScrollbar({
138
138
  enabled: fixedScrollEnabled,
139
+ layoutKey: horizontalScroll,
139
140
  scrollContainerRef,
140
141
  scrollAreaRef,
142
+ scrollbarRef,
141
143
  thumbRef,
142
144
  });
143
145
  const { handleHThumbMouseDown } = useTableHorizontalScrollbar({
@@ -6,11 +6,13 @@ export declare const H_SCROLLBAR_SIDE_GAP = 8;
6
6
  export declare const SCROLLBAR_TRACK_OFFSET = 16;
7
7
  type VerticalScrollbarOptions = {
8
8
  enabled: boolean;
9
+ layoutKey: unknown;
9
10
  scrollContainerRef: RefObject<HTMLDivElement | null>;
10
11
  scrollAreaRef: RefObject<HTMLDivElement | null>;
12
+ scrollbarRef: RefObject<HTMLDivElement | null>;
11
13
  thumbRef: RefObject<HTMLDivElement | null>;
12
14
  };
13
- export declare const useTableVerticalScrollbar: ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }: VerticalScrollbarOptions) => {
15
+ export declare const useTableVerticalScrollbar: ({ enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef, }: VerticalScrollbarOptions) => {
14
16
  handleThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
15
17
  };
16
18
  type HorizontalScrollbarOptions = {
@@ -51,7 +51,8 @@ const startDrag = (e, options) => {
51
51
  document.addEventListener('mousemove', onMove);
52
52
  document.addEventListener('mouseup', onUp);
53
53
  };
54
- export const useTableVerticalScrollbar = ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }) => {
54
+ export const useTableVerticalScrollbar = ({ enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef, }) => {
55
+ // biome-ignore lint/correctness/useExhaustiveDependencies: layoutKey는 직접 참조하지 않지만 horizontalScroll 토글로 스크롤바 DOM이 리마운트될 때 effect를 재바인딩하기 위한 트리거로 의도적으로 포함한다.
55
56
  useEffect(() => {
56
57
  if (!enabled)
57
58
  return;
@@ -65,7 +66,8 @@ export const useTableVerticalScrollbar = ({ enabled, scrollContainerRef, scrollA
65
66
  thumbEl.style.height = '0';
66
67
  return;
67
68
  }
68
- const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
69
+ const trackHeight = scrollbarRef.current?.clientHeight ||
70
+ (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
69
71
  const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, (clientHeight / scrollHeight) * trackHeight);
70
72
  const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
71
73
  thumbEl.style.height = `${thumbHeight}px`;
@@ -76,12 +78,14 @@ export const useTableVerticalScrollbar = ({ enabled, scrollContainerRef, scrollA
76
78
  observer.observe(scrollEl);
77
79
  if (scrollAreaRef.current)
78
80
  observer.observe(scrollAreaRef.current);
81
+ if (scrollbarRef.current)
82
+ observer.observe(scrollbarRef.current);
79
83
  update();
80
84
  return () => {
81
85
  scrollEl.removeEventListener('scroll', update);
82
86
  observer.disconnect();
83
87
  };
84
- }, [enabled, scrollContainerRef, scrollAreaRef, thumbRef]);
88
+ }, [enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef]);
85
89
  const handleThumbMouseDown = (e) => {
86
90
  const scrollEl = scrollContainerRef.current;
87
91
  const thumbEl = thumbRef.current;
@@ -1,11 +1,12 @@
1
1
  import type { ColorTone } from '../../../../constant/color';
2
2
  import type { Size } from '../../../../constant/size';
3
3
  import type { SideSlotType } from '../../../types/side-slot';
4
- type BadgeType = 'pill-outline' | 'pill-dark-color';
4
+ type BadgeType = 'pill-outline' | 'pill-dark-color' | 'new-badge';
5
5
  type BadgeColor = Extract<ColorTone, 'neutral' | 'error' | 'warning' | 'success' | 'blue' | 'pink' | 'disabled'>;
6
6
  type BadgeSize = Extract<Size, 'xs' | 'sm' | 'md'>;
7
7
  type BadgeProps = {
8
- label: string;
8
+ /** new-badge 타입에서는 사용되지 않는다(무시됨). 그 외 타입에서는 필수로 전달한다. */
9
+ label?: string;
9
10
  type?: BadgeType;
10
11
  color?: BadgeColor;
11
12
  className?: string;
@@ -1,11 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { New } from '@ncds/ui-admin-icon';
2
3
  import classNames from 'classnames';
3
4
  import { sideSlotRender } from './utils';
4
5
  /**
5
6
  * 뱃지 컴포넌트의 아이콘은 디자인 시스템에서 12px 고정으로 정의되어 있습니다.
6
7
  */
7
8
  const BADGE_ICON_SIZE = 12;
9
+ /** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
10
+ const NEW_BADGE_ICON_SIZE = { sm: 12, md: 16 };
8
11
  const Badge = ({ label, type = 'pill-outline', color = 'neutral', className, leadingIcon, trailingIcon, size = 'xs', }) => {
12
+ // new-badge: 신규 콘텐츠 'N' 마크 전용 타입. label·color·leadingIcon·trailingIcon은 무시되고
13
+ // 색은 --pink-600 으로 고정된다. (디자이너 명세: Icon/FeaturedIcon/Badge로 대체 불가한 전용 표시)
14
+ if (type === 'new-badge') {
15
+ const newBadgeSize = size === 'md' ? 'md' : 'sm';
16
+ const iconSize = NEW_BADGE_ICON_SIZE[newBadgeSize];
17
+ return (_jsx("span", { className: classNames('ncua-badge', 'ncua-badge--new-badge', `ncua-badge--new-badge-${newBadgeSize}`, className), children: _jsx(New, { width: iconSize, height: iconSize }) }));
18
+ }
9
19
  return (_jsxs("span", { className: classNames('ncua-badge', `ncua-badge--${type}`, `ncua-badge--${color}`, `ncua-badge--${size}`, className), children: [leadingIcon && sideSlotRender({ slot: leadingIcon, defaultIconSize: BADGE_ICON_SIZE }), _jsx("span", { className: "ncua-badge__label", children: label }), trailingIcon && sideSlotRender({ slot: trailingIcon, defaultIconSize: BADGE_ICON_SIZE })] }));
10
20
  };
11
21
  export { Badge };
@@ -30,6 +30,10 @@ interface ComboBoxProps extends Omit<ComponentPropsWithRef<'div'>, 'size' | 'onC
30
30
  */
31
31
  maxSelection?: number | null;
32
32
  onEdit?: () => void;
33
+ /**
34
+ * 완료 버튼 클릭 시 호출되는 콜백 — 현재 선택된 값을 인자로 받음 (multiple 모드에서만 트리거)
35
+ */
36
+ onComplete?: OptionChangeHandler;
33
37
  }
34
38
  declare const ComboBox: import("react").ForwardRefExoticComponent<Omit<ComboBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
35
39
  export { defaultMaxHeight, ComboBox };
@@ -20,7 +20,7 @@ const notifyFormChange = (register, multiple, newValue) => {
20
20
  });
21
21
  }
22
22
  };
23
- const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, hintText, children, size = 'xs', destructive = false, value, optionItems = [], onChange, onSearch, disabled = false, register, maxHeight = defaultMaxHeight, searchValue = '', label, required = false, multiple = false, showFooterButtons = false, maxSelection, onEdit, ...props }, ref) => {
23
+ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, hintText, children, size = 'xs', destructive = false, value, optionItems = [], onChange, onSearch, disabled = false, register, maxHeight = defaultMaxHeight, searchValue = '', label, required = false, multiple = false, showFooterButtons = false, maxSelection, onEdit, onComplete, ...props }, ref) => {
24
24
  const internalRef = useRef(null);
25
25
  const dropdownRef = useRef(null);
26
26
  const inputRef = useRef(null);
@@ -122,13 +122,14 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
122
122
  const handleEdit = () => {
123
123
  onEdit?.();
124
124
  };
125
- const handleComplete = () => {
125
+ const handleComplete = (completedValue) => {
126
126
  if (!multiple)
127
127
  return;
128
128
  const tags = getSelectedTagsData();
129
129
  setSelectedTags(tags);
130
130
  setInputValue('');
131
131
  closeDropdown();
132
+ onComplete?.(completedValue);
132
133
  };
133
134
  const handleRemoveTag = (tagId) => {
134
135
  if (!onChange)
@@ -7,6 +7,18 @@ import { Tooltip } from '../../overlays/tooltip';
7
7
  import { HintText, Label } from '../../shared';
8
8
  import { FileInputErrorType as ImageFileInputErrorType } from '../file-input/FileInput';
9
9
  import { ImagePreview } from './components/ImagePreview';
10
+ const toInvalidFile = (file, errorType) => ({
11
+ name: file.name,
12
+ size: file.size,
13
+ type: file.type,
14
+ lastModified: file.lastModified,
15
+ webkitRelativePath: file.webkitRelativePath,
16
+ arrayBuffer: () => file.arrayBuffer(),
17
+ stream: () => file.stream(),
18
+ text: () => file.text(),
19
+ slice: (...args) => file.slice(...args),
20
+ errorType,
21
+ });
10
22
  export const ImageFileInput = forwardRef(({ size = 'sm', accept, multiple = false, maxFileSize, maxFileCount, value, onChange, onFileSelect, onFail, buttonLabel = '파일 찾기', imagePreviewTooltipLabel = '이미지 업로드', disabled, label, hintItems, validation, destructive, isRequired, showHelpIcon, hintText, showFileTagList = true, showHintText = true, showFileInput = true, ...props }, ref) => {
11
23
  const fileInputRef = useRef(null);
12
24
  useImperativeHandle(ref, () => fileInputRef.current);
@@ -48,25 +60,16 @@ export const ImageFileInput = forwardRef(({ size = 'sm', accept, multiple = fals
48
60
  const invalidFiles = [];
49
61
  for (const file of fileList) {
50
62
  if (files.some((f) => f.name === file.name && f.size === file.size)) {
51
- invalidFiles.push({
52
- ...file,
53
- errorType: ImageFileInputErrorType.ALREADY_UPLOADED,
54
- });
63
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.ALREADY_UPLOADED));
55
64
  continue;
56
65
  }
57
66
  if (!!maxFileSize && file.size > maxFileSize) {
58
- invalidFiles.push({
59
- ...file,
60
- errorType: ImageFileInputErrorType.EXCEED_MAX_FILE_SIZE,
61
- });
67
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.EXCEED_MAX_FILE_SIZE));
62
68
  continue;
63
69
  }
64
70
  // Skip max count check if maxFileCount is 1 (allow replacement)
65
71
  if (!!maxFileCount && maxFileCount !== 1 && files.length + validFiles.length >= maxFileCount) {
66
- invalidFiles.push({
67
- ...file,
68
- errorType: ImageFileInputErrorType.EXCEED_MAX_FILE_COUNT,
69
- });
72
+ invalidFiles.push(toInvalidFile(file, ImageFileInputErrorType.EXCEED_MAX_FILE_COUNT));
70
73
  continue;
71
74
  }
72
75
  validFiles.push(file);
@@ -85,12 +88,12 @@ export const ImageFileInput = forwardRef(({ size = 'sm', accept, multiple = fals
85
88
  };
86
89
  const renderImagePreview = (files = []) => {
87
90
  const showEmptySlot = maxFileCount ? files.length < maxFileCount : files.length === 0;
88
- return (_jsxs("div", { className: "ncua-image-file-input__previews", children: [files.map((file, index) => (_jsx(ImagePreview, { file: file, onRemove: () => handleRemoveFile(index) }, `${file.name}-${index}`))), showEmptySlot && (_jsxs("div", { className: "ncua-image-file-input__empty-slot-wrapper", onMouseEnter: () => !disabled && setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), onClick: handleBrowseClick, children: [_jsx(Button, { onlyIcon: true, size: size, className: classNames('ncua-image-file-input__preview-container'), onClick: handleBrowseClick, disabled: disabled, label: imagePreviewTooltipLabel }), _jsx(Tooltip, { content: imagePreviewTooltipLabel, position: "bottom", tooltipType: "black", forceVisible: isButtonHovered && !disabled, disablePortal: true })] }))] }));
91
+ return (_jsxs("div", { className: "ncua-image-file-input__previews", children: [files.map((file, index) => (_jsx(ImagePreview, { file: file, onRemove: () => handleRemoveFile(index) }, `${file.name}-${index}`))), showEmptySlot && (_jsxs("div", { className: "ncua-image-file-input__empty-slot-wrapper", onMouseEnter: () => !disabled && setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), onClick: handleBrowseClick, children: [_jsx(Button, { onlyIcon: true, size: size, className: classNames('ncua-image-file-input__preview-container'), onClick: handleBrowseClick, disabled: disabled, label: imagePreviewTooltipLabel }), _jsx(Tooltip, { content: imagePreviewTooltipLabel, position: "bottom", tooltipType: "black", forceVisible: isButtonHovered && !disabled, panelClassName: classNames('ncua-image-file-input__slot-tooltip', `ncua-image-file-input__slot-tooltip--${size}`) })] }))] }));
89
92
  };
90
93
  const renderHintList = () => {
91
94
  if (!hintItems || hintItems.length === 0)
92
95
  return null;
93
96
  return (_jsx("ul", { className: "ncua-file-input__hint-list", children: hintItems.map((hint) => (_jsx("li", { className: "ncua-file-input__hint-item", children: hint }, hint))) }));
94
97
  };
95
- return (_jsxs("div", { className: classNames('ncua-image-file-input', `ncua-image-file-input--${size}`, { destructive: destructive }), children: [renderImagePreview(files), showFileInput && (_jsxs("div", { className: classNames('ncua-file-input', `ncua-file-input--${size}`), children: [_jsx("input", { hidden: true, ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, tabIndex: -1, "aria-hidden": "true", ...props }), _jsxs("div", { className: "ncua-file-input__input-container", children: [_jsxs("div", { className: "ncua-file-input__label", children: [_jsx(Label, { isRequired: isRequired, children: label }), showHelpIcon && _jsx(HelpCircle, { className: "ncua-input__help-icon" })] }), _jsx(Button, { size: "xs", onClick: handleBrowseClick, disabled: disabled, leadingIcon: { type: 'icon', icon: Upload01 }, label: buttonLabel }), showHintText && hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), showHintText && renderHintList()] }))] }));
98
+ return (_jsxs("div", { className: classNames('ncua-image-file-input', `ncua-image-file-input--${size}`, { destructive: destructive }), children: [renderImagePreview(files), _jsx("input", { hidden: true, ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, tabIndex: -1, "aria-hidden": "true", ...props }), showFileInput && (_jsxs("div", { className: classNames('ncua-file-input', `ncua-file-input--${size}`), children: [_jsxs("div", { className: "ncua-file-input__input-container", children: [_jsxs("div", { className: "ncua-file-input__label", children: [_jsx(Label, { isRequired: isRequired, children: label }), showHelpIcon && _jsx(HelpCircle, { className: "ncua-input__help-icon" })] }), _jsx(Button, { size: "xs", onClick: handleBrowseClick, disabled: disabled, leadingIcon: { type: 'icon', icon: Upload01 }, label: buttonLabel }), showHintText && hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), showHintText && renderHintList()] }))] }));
96
99
  });
@@ -28,6 +28,10 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
28
28
  */
29
29
  maxSelection?: number | null;
30
30
  onEdit?: () => void;
31
+ /**
32
+ * 완료 버튼 클릭 시 호출되는 콜백 — 현재 선택된 값을 인자로 받음 (multiple 모드에서만 트리거)
33
+ */
34
+ onComplete?: OptionChangeHandler;
31
35
  align?: 'left' | 'right';
32
36
  /**
33
37
  * 옵션 패널을 React Portal로 body에 렌더한다.
@@ -40,7 +40,7 @@ function DisplayValue({ displayValue }) {
40
40
  }
41
41
  return (_jsxs("div", { className: "ncua-selectbox__value-container", children: [displayValue.icon && (_jsx("span", { className: "ncua-selectbox__value-icon", children: _jsx(displayValue.icon, { width: 16, height: 16 }) })), _jsx("span", { className: "ncua-selectbox__value-text", children: displayValue.label })] }));
42
42
  }
43
- const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, maxSelection, align = 'left', usePortal, id, className, children, register, onChange, onEdit, ...props }, ref
43
+ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceholder = false, hintText, size = 'xs', type = 'default', autoWidth = true, destructive = false, value, optionItems = [], disabled = false, maxHeight = DEFAULT_MAX_HEIGHT, multiple = false, maxSelection, align = 'left', usePortal, id, className, children, register, onChange, onEdit, onComplete, ...props }, ref
44
44
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 옵션/멀티/태그/포탈 등 필수 분기 통합
45
45
  ) => {
46
46
  const internalRef = useRef(null);
@@ -106,10 +106,11 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
106
106
  const handleEdit = () => {
107
107
  onEdit?.();
108
108
  };
109
- const handleComplete = () => {
109
+ const handleComplete = (completedValue) => {
110
110
  if (multiple) {
111
111
  const tags = getSelectedTagsData();
112
112
  setSelectedTags(tags);
113
+ onComplete?.(completedValue);
113
114
  }
114
115
  closeDropdown();
115
116
  };
@@ -16,6 +16,7 @@ interface TooltipProps {
16
16
  forceVisible?: boolean;
17
17
  /** true이면 Portal 없이 앵커 내부에 패널을 인라인 렌더링. CSS 기반 위치 지정이 필요한 경우 사용 */
18
18
  disablePortal?: boolean;
19
+ panelClassName?: string;
19
20
  }
20
- export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, forceVisible, disablePortal, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
21
+ export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, forceVisible, disablePortal, panelClassName, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
21
22
  export {};
@@ -86,7 +86,7 @@ const renderIcon = (iconStyle, iconType, iconSize, iconColor) => {
86
86
  }
87
87
  return iconType === 'stroke' ? (_jsx(AlertCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(AlertCircleFill, { width: iconSize, height: iconSize, color: iconColor }));
88
88
  };
89
- export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position = 'auto', size = 'sm', title, content, hideArrow = false, type = 'short', iconColor = 'var(--gray-300)', iconStyle = 'help-circle', className, zIndex, forceVisible, disablePortal = false, }) => {
89
+ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position = 'auto', size = 'sm', title, content, hideArrow = false, type = 'short', iconColor = 'var(--gray-300)', iconStyle = 'help-circle', className, zIndex, forceVisible, disablePortal = false, panelClassName, }) => {
90
90
  const iconSize = size === 'sm' ? ICON_SIZE_SM : ICON_SIZE_DEFAULT;
91
91
  const anchorRef = useRef(null);
92
92
  const panelRef = useRef(null);
@@ -154,12 +154,12 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
154
154
  'ncua-tooltip--stroke': iconType === 'stroke',
155
155
  'ncua-tooltip--auto': position === 'auto',
156
156
  }, className), [size, type, hideArrow, iconType, position, className]);
157
- const panelClassName = useMemo(() => classNames({ 'ncua-tooltip-panel': !disablePortal }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
157
+ const panelClasses = useMemo(() => classNames({ 'ncua-tooltip-panel': !disablePortal }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
158
158
  'ncua-tooltip__bg--visible': effectiveVisible,
159
159
  'ncua-tooltip__bg--force-hidden': isManuallyClose && !forceVisible,
160
- }), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal]);
160
+ }, panelClassName), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal, panelClassName]);
161
161
  const panelStyle = buildPanelStyle(disablePortal, coords, effectiveVisible, zIndex);
162
162
  const portalTarget = mounted ? resolvePortalTarget() : null;
163
- const panel = (_jsxs("span", { ref: panelRef, className: panelClassName, style: panelStyle, children: [title && _jsx("span", { className: "ncua-tooltip__title", children: title }), content && _jsx("span", { className: "ncua-tooltip__content", children: content }), type === 'long' && (_jsx(ButtonCloseX, { className: "ncua-tooltip__close-button", size: "xs", theme: tooltipType === 'white' ? 'dark' : 'light', onClick: handleCloseClick, "aria-label": "\uD234\uD301 \uB2EB\uAE30" }))] }));
163
+ const panel = (_jsxs("span", { ref: panelRef, className: panelClasses, style: panelStyle, children: [title && _jsx("span", { className: "ncua-tooltip__title", children: title }), content && _jsx("span", { className: "ncua-tooltip__content", children: content }), type === 'long' && (_jsx(ButtonCloseX, { className: "ncua-tooltip__close-button", size: "xs", theme: tooltipType === 'white' ? 'dark' : 'light', onClick: handleCloseClick, "aria-label": "\uD234\uD301 \uB2EB\uAE30" }))] }));
164
164
  return (_jsxs("span", { ref: anchorRef, className: tooltipClassName, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [renderIcon(iconStyle, iconType, iconSize, iconColor), disablePortal ? panel : portalTarget && createPortal(panel, portalTarget)] }));
165
165
  };
@@ -25,7 +25,7 @@ type SelectDropdownProps = ComponentPropsWithRef<'div'> & {
25
25
  showSelectAllAction?: boolean;
26
26
  onSelectAll?: () => void;
27
27
  onEdit?: () => void;
28
- onComplete?: () => void;
28
+ onComplete?: (value: OptionValue) => void;
29
29
  activeDescendantId?: string;
30
30
  componentType?: 'selectbox' | 'combobox';
31
31
  align?: 'left' | 'right';
@@ -20,7 +20,7 @@ const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', op
20
20
  'ncua-select-dropdown__option--selected': isSelected,
21
21
  'ncua-select-dropdown__option--focused': isFocused,
22
22
  }), onClick: () => onOptionSelect(option), onMouseEnter: handleMouseEnter, role: "option", "aria-selected": isSelected, children: [option.icon && (_jsx("span", { className: "ncua-select-dropdown__option-icon", children: _jsx(option.icon, { width: 16, height: 16 }) })), _jsx("span", { className: "ncua-select-dropdown__option-text", children: option.label })] }, option.id));
23
- }), children] }) }), showFooterButtons && (_jsx("div", { className: "ncua-select-dropdown__footer", children: _jsxs("div", { className: "ncua-select-dropdown__footer-buttons", children: [_jsx("div", { className: "ncua-select-dropdown__footer-left", children: multiple && showSelectAllAction && (_jsx(Button, { label: selectAllButtonText, hierarchy: "text", size: "xs", onClick: onSelectAll, underline: true })) }), _jsxs("div", { className: "ncua-select-dropdown__footer-right", children: [_jsx(Button, { label: "\uD3B8\uC9D1", hierarchy: "secondary-gray", size: "xs", onClick: onEdit }), multiple && _jsx(Button, { label: "\uC120\uD0DD \uC644\uB8CC", hierarchy: "secondary", size: "xs", onClick: onComplete })] })] }) }))] }));
23
+ }), children] }) }), showFooterButtons && (_jsx("div", { className: "ncua-select-dropdown__footer", children: _jsxs("div", { className: "ncua-select-dropdown__footer-buttons", children: [_jsx("div", { className: "ncua-select-dropdown__footer-left", children: multiple && showSelectAllAction && (_jsx(Button, { label: selectAllButtonText, hierarchy: "text", size: "xs", onClick: onSelectAll, underline: true })) }), _jsxs("div", { className: "ncua-select-dropdown__footer-right", children: [_jsx(Button, { label: "\uD3B8\uC9D1", hierarchy: "secondary-gray", size: "xs", onClick: onEdit }), multiple && (_jsx(Button, { label: "\uC120\uD0DD \uC644\uB8CC", hierarchy: "secondary", size: "xs", onClick: () => onComplete?.(value ?? []) }))] })] }) }))] }));
24
24
  });
25
25
  SelectDropdown.displayName = 'SelectDropdown';
26
26
  export { SelectDropdown };
@@ -6,11 +6,13 @@ export declare const H_SCROLLBAR_SIDE_GAP = 8;
6
6
  export declare const SCROLLBAR_TRACK_OFFSET = 16;
7
7
  type VerticalScrollbarOptions = {
8
8
  enabled: boolean;
9
+ layoutKey: unknown;
9
10
  scrollContainerRef: RefObject<HTMLDivElement | null>;
10
11
  scrollAreaRef: RefObject<HTMLDivElement | null>;
12
+ scrollbarRef: RefObject<HTMLDivElement | null>;
11
13
  thumbRef: RefObject<HTMLDivElement | null>;
12
14
  };
13
- export declare const useTableVerticalScrollbar: ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }: VerticalScrollbarOptions) => {
15
+ export declare const useTableVerticalScrollbar: ({ enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef, }: VerticalScrollbarOptions) => {
14
16
  handleThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
15
17
  };
16
18
  type HorizontalScrollbarOptions = {
@@ -1,11 +1,12 @@
1
1
  import type { ColorTone } from '../../../../constant/color';
2
2
  import type { Size } from '../../../../constant/size';
3
3
  import type { SideSlotType } from '../../../types/side-slot';
4
- type BadgeType = 'pill-outline' | 'pill-dark-color';
4
+ type BadgeType = 'pill-outline' | 'pill-dark-color' | 'new-badge';
5
5
  type BadgeColor = Extract<ColorTone, 'neutral' | 'error' | 'warning' | 'success' | 'blue' | 'pink' | 'disabled'>;
6
6
  type BadgeSize = Extract<Size, 'xs' | 'sm' | 'md'>;
7
7
  type BadgeProps = {
8
- label: string;
8
+ /** new-badge 타입에서는 사용되지 않는다(무시됨). 그 외 타입에서는 필수로 전달한다. */
9
+ label?: string;
9
10
  type?: BadgeType;
10
11
  color?: BadgeColor;
11
12
  className?: string;
@@ -30,6 +30,10 @@ interface ComboBoxProps extends Omit<ComponentPropsWithRef<'div'>, 'size' | 'onC
30
30
  */
31
31
  maxSelection?: number | null;
32
32
  onEdit?: () => void;
33
+ /**
34
+ * 완료 버튼 클릭 시 호출되는 콜백 — 현재 선택된 값을 인자로 받음 (multiple 모드에서만 트리거)
35
+ */
36
+ onComplete?: OptionChangeHandler;
33
37
  }
34
38
  declare const ComboBox: import("react").ForwardRefExoticComponent<Omit<ComboBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
35
39
  export { defaultMaxHeight, ComboBox };
@@ -28,6 +28,10 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
28
28
  */
29
29
  maxSelection?: number | null;
30
30
  onEdit?: () => void;
31
+ /**
32
+ * 완료 버튼 클릭 시 호출되는 콜백 — 현재 선택된 값을 인자로 받음 (multiple 모드에서만 트리거)
33
+ */
34
+ onComplete?: OptionChangeHandler;
31
35
  align?: 'left' | 'right';
32
36
  /**
33
37
  * 옵션 패널을 React Portal로 body에 렌더한다.
@@ -16,6 +16,7 @@ interface TooltipProps {
16
16
  forceVisible?: boolean;
17
17
  /** true이면 Portal 없이 앵커 내부에 패널을 인라인 렌더링. CSS 기반 위치 지정이 필요한 경우 사용 */
18
18
  disablePortal?: boolean;
19
+ panelClassName?: string;
19
20
  }
20
- export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, forceVisible, disablePortal, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
21
+ export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, forceVisible, disablePortal, panelClassName, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
21
22
  export {};
@@ -25,7 +25,7 @@ type SelectDropdownProps = ComponentPropsWithRef<'div'> & {
25
25
  showSelectAllAction?: boolean;
26
26
  onSelectAll?: () => void;
27
27
  onEdit?: () => void;
28
- onComplete?: () => void;
28
+ onComplete?: (value: OptionValue) => void;
29
29
  activeDescendantId?: string;
30
30
  componentType?: 'selectbox' | 'combobox';
31
31
  align?: 'left' | 'right';
@@ -5017,6 +5017,20 @@ button {
5017
5017
  color: var(--gray-300);
5018
5018
  background-color: var(--gray-100);
5019
5019
  }
5020
+ .ncua-badge--new-badge {
5021
+ justify-content: center;
5022
+ color: var(--pink-600);
5023
+ background-color: var(--pink-200);
5024
+ border-radius: 5px;
5025
+ }
5026
+ .ncua-badge--new-badge-sm {
5027
+ width: 16px;
5028
+ height: 16px;
5029
+ }
5030
+ .ncua-badge--new-badge-md {
5031
+ width: 20px;
5032
+ height: 20px;
5033
+ }
5020
5034
 
5021
5035
  .ncua-badge-group {
5022
5036
  display: inline-flex;
@@ -5888,6 +5902,14 @@ button {
5888
5902
  gap: 16px;
5889
5903
  }
5890
5904
 
5905
+ .ncua-tooltip-panel.ncua-image-file-input__slot-tooltip--xs {
5906
+ transform: translateY(-19px);
5907
+ }
5908
+
5909
+ .ncua-tooltip-panel.ncua-image-file-input__slot-tooltip--sm {
5910
+ transform: translateY(-27px);
5911
+ }
5912
+
5891
5913
  .ncua-table-wrapper {
5892
5914
  display: flex;
5893
5915
  flex-direction: column;
@@ -5957,6 +5979,8 @@ button {
5957
5979
  }
5958
5980
 
5959
5981
  .ncua-table {
5982
+ --ncua-table-header-height: 40px;
5983
+ --ncua-table-default-min-width: 1140px;
5960
5984
  position: relative;
5961
5985
  display: flex;
5962
5986
  flex-direction: column;
@@ -6308,8 +6332,7 @@ button {
6308
6332
  .ncua-table--vertical .ncua-table .ncua-table__body > .ncua-table__row:last-child > .ncua-table__cell:last-child {
6309
6333
  border-bottom-right-radius: 0;
6310
6334
  }
6311
- .ncua-table--vertical .ncua-table__body > .ncua-table__row td,
6312
- .ncua-table--vertical .ncua-table__body > .ncua-table__row th {
6335
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row {
6313
6336
  height: 48px;
6314
6337
  }
6315
6338
  .ncua-table--vertical .ncua-table__body > .ncua-table__row td:first-child,
@@ -6332,6 +6355,42 @@ button {
6332
6355
  text-align: left;
6333
6356
  font-weight: var(--font-weights-commerce-sans-0);
6334
6357
  }
6358
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row td:first-child > div:has(> .ncua-tooltip),
6359
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row th:first-child > div:has(> .ncua-tooltip) {
6360
+ display: flex;
6361
+ align-items: center;
6362
+ }
6363
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row td:first-child > div:has(> .ncua-tooltip) > .ncua-tooltip,
6364
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row th:first-child > div:has(> .ncua-tooltip) > .ncua-tooltip {
6365
+ display: inline-flex;
6366
+ align-items: center;
6367
+ line-height: 1;
6368
+ margin-left: var(--spacing-xs);
6369
+ }
6370
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row td:first-child > div:has(> .ncua-tooltip) > .ncua-tooltip > .ncua-tooltip__icon,
6371
+ .ncua-table--vertical .ncua-table__body > .ncua-table__row th:first-child > div:has(> .ncua-tooltip) > .ncua-tooltip > .ncua-tooltip__icon {
6372
+ display: inline-flex;
6373
+ align-items: center;
6374
+ line-height: 1;
6375
+ }
6376
+ .ncua-table--vertical .ncua-table .ncua-table__header > .ncua-table__row th:first-child {
6377
+ width: auto;
6378
+ min-width: 0;
6379
+ max-width: none;
6380
+ padding: 0 var(--spacing-m);
6381
+ font-size: var(--font-size-xs);
6382
+ font-weight: var(--font-weights-commerce-sans-1);
6383
+ line-height: var(--line-heights-xs);
6384
+ color: var(--gray-500);
6385
+ text-align: center;
6386
+ background: var(--gray-100);
6387
+ border-right: 1px solid var(--gray-200);
6388
+ }
6389
+ .ncua-table--vertical .ncua-table .ncua-table__header > .ncua-table__row th:last-child {
6390
+ padding: 0 var(--spacing-m);
6391
+ text-align: center;
6392
+ font-weight: var(--font-weights-commerce-sans-1);
6393
+ }
6335
6394
  .ncua-table--vertical .ncua-table .ncua-table__body > .ncua-table__row td:first-child,
6336
6395
  .ncua-table--vertical .ncua-table .ncua-table__body > .ncua-table__row th:first-child {
6337
6396
  width: auto;
@@ -6355,7 +6414,7 @@ button {
6355
6414
  color: var(--primary-red-500, #ec1d31);
6356
6415
  font-size: var(--font-size-sm);
6357
6416
  font-weight: var(--font-weights-commerce-sans-1);
6358
- margin-right: 2px;
6417
+ margin-right: 4px;
6359
6418
  }
6360
6419
 
6361
6420
  .ncua-data-grid {
@@ -6476,7 +6535,15 @@ button {
6476
6535
  border: 1px solid var(--gray-100);
6477
6536
  border-radius: 12px;
6478
6537
  box-shadow: var(--shadow-sm);
6479
- overflow: hidden;
6538
+ }
6539
+ .ncua-block-container > *:first-child {
6540
+ border-radius: 12px 12px 0 0;
6541
+ }
6542
+ .ncua-block-container > *:last-child {
6543
+ border-radius: 0 0 12px 12px;
6544
+ }
6545
+ .ncua-block-container > *:only-child, .ncua-block-container > *:first-child:has(~ *:last-child[style*="display: none"]), .ncua-block-container > *:first-child:has(~ *:last-child[hidden]) {
6546
+ border-radius: 12px;
6480
6547
  }
6481
6548
  .ncua-block-container__body {
6482
6549
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncds/ui-admin",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "description": "nhn-commerce의 어드민 디자인 시스템입니다.",
5
5
  "scripts": {
6
6
  "barrel": "node barrel.js",
@@ -70,7 +70,7 @@
70
70
  "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.0",
71
71
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
72
72
  "@atlaskit/pragmatic-drag-and-drop-react-accessibility": "1.1.4",
73
- "@ncds/ui-admin-icon": "0.1.9",
73
+ "@ncds/ui-admin-icon": "0.1.11",
74
74
  "classnames": "2.5.1",
75
75
  "dompurify": "3.4.1",
76
76
  "flatpickr": "4.6.13",