@ncds/ui-admin 1.8.3 → 1.8.5

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 (217) hide show
  1. package/dist/cjs/assets/scripts/featuredIcon.js +87 -0
  2. package/dist/cjs/assets/scripts/notification/FloatingNotification.js +178 -0
  3. package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +133 -0
  4. package/dist/cjs/assets/scripts/notification/MessageNotification.js +159 -0
  5. package/dist/cjs/assets/scripts/notification/Notification.js +120 -0
  6. package/dist/cjs/assets/scripts/notification/const/classNames.js +50 -0
  7. package/dist/cjs/assets/scripts/notification/const/icons.js +31 -0
  8. package/dist/cjs/assets/scripts/notification/const/index.js +87 -0
  9. package/dist/cjs/assets/scripts/notification/const/sizes.js +46 -0
  10. package/dist/cjs/assets/scripts/notification/const/types.js +14 -0
  11. package/dist/cjs/assets/scripts/notification/index.js +116 -0
  12. package/dist/cjs/assets/scripts/notification/positionSync.js +180 -0
  13. package/dist/cjs/assets/scripts/notification/utils.js +122 -0
  14. package/dist/cjs/assets/scripts/shared/ButtonCloseX.js +45 -0
  15. package/dist/cjs/assets/scripts/utils/sanitize.js +39 -0
  16. package/dist/cjs/src/components/data-display/data-grid/DataGrid.js +5 -1
  17. package/dist/cjs/src/components/data-display/table/Table.js +118 -96
  18. package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +187 -0
  19. package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
  20. package/dist/cjs/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
  21. package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +67 -29
  22. package/dist/cjs/src/components/index.js +33 -0
  23. package/dist/cjs/src/components/layout/block-container/BlockContainer.js +38 -0
  24. package/dist/cjs/src/components/layout/block-container/index.js +16 -0
  25. package/dist/cjs/src/components/layout/block-header/BlockHeader.js +107 -0
  26. package/dist/cjs/src/components/layout/block-header/SubTitle.js +56 -0
  27. package/dist/cjs/src/components/layout/block-header/index.js +27 -0
  28. package/dist/cjs/src/components/layout/page-title/PageTitle.js +95 -0
  29. package/dist/cjs/src/components/layout/page-title/index.js +16 -0
  30. package/dist/cjs/src/components/overlays/dropdown/Dropdown.js +47 -19
  31. package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +25 -0
  32. package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +86 -13
  33. package/dist/cjs/src/components/overlays/notification/Notification.js +7 -0
  34. package/dist/cjs/src/components/overlays/notification/host.js +12 -0
  35. package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +57 -44
  36. package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +2 -1
  37. package/dist/cjs/src/contexts/FloatingContext.js +11 -0
  38. package/dist/cjs/src/contexts/index.js +16 -0
  39. package/dist/cjs/src/hooks/index.js +11 -0
  40. package/dist/cjs/src/hooks/useFloatingPosition.js +78 -0
  41. package/dist/cjs/src/hooks/usePortalState.js +17 -0
  42. package/dist/cjs/src/utils/dropdown/maxSelection.js +35 -0
  43. package/dist/cjs/src/utils/dropdown/multiSelect.js +72 -15
  44. package/dist/esm/assets/scripts/featuredIcon.js +80 -0
  45. package/dist/esm/assets/scripts/notification/FloatingNotification.js +171 -0
  46. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +126 -0
  47. package/dist/esm/assets/scripts/notification/MessageNotification.js +152 -0
  48. package/dist/esm/assets/scripts/notification/Notification.js +113 -0
  49. package/dist/esm/assets/scripts/notification/const/classNames.js +44 -0
  50. package/dist/esm/assets/scripts/notification/const/icons.js +25 -0
  51. package/dist/esm/assets/scripts/notification/const/index.js +4 -0
  52. package/dist/esm/assets/scripts/notification/const/sizes.js +40 -0
  53. package/dist/esm/assets/scripts/notification/const/types.js +8 -0
  54. package/dist/esm/assets/scripts/notification/index.js +10 -0
  55. package/dist/esm/assets/scripts/notification/positionSync.js +171 -0
  56. package/dist/esm/assets/scripts/notification/utils.js +109 -0
  57. package/dist/esm/assets/scripts/shared/ButtonCloseX.js +37 -0
  58. package/dist/esm/assets/scripts/utils/sanitize.js +31 -0
  59. package/dist/esm/src/components/data-display/data-grid/DataGrid.js +5 -1
  60. package/dist/esm/src/components/data-display/table/Table.js +118 -96
  61. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +179 -0
  62. package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
  63. package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
  64. package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +67 -29
  65. package/dist/esm/src/components/index.js +3 -0
  66. package/dist/esm/src/components/layout/block-container/BlockContainer.js +31 -0
  67. package/dist/esm/src/components/layout/block-container/index.js +1 -0
  68. package/dist/esm/src/components/layout/block-header/BlockHeader.js +100 -0
  69. package/dist/esm/src/components/layout/block-header/SubTitle.js +49 -0
  70. package/dist/esm/src/components/layout/block-header/index.js +2 -0
  71. package/dist/esm/src/components/layout/page-title/PageTitle.js +88 -0
  72. package/dist/esm/src/components/layout/page-title/index.js +1 -0
  73. package/dist/esm/src/components/overlays/dropdown/Dropdown.js +47 -19
  74. package/dist/esm/src/components/overlays/notification/CalloutNotification.js +19 -0
  75. package/dist/esm/src/components/overlays/notification/FloatingNotification.js +86 -14
  76. package/dist/esm/src/components/overlays/notification/Notification.js +7 -0
  77. package/dist/esm/src/components/overlays/notification/host.js +9 -0
  78. package/dist/esm/src/components/overlays/tooltip/Tooltip.js +58 -45
  79. package/dist/esm/src/components/select-dropdown/SelectDropdown.js +2 -1
  80. package/dist/esm/src/contexts/FloatingContext.js +4 -0
  81. package/dist/esm/src/contexts/index.js +1 -0
  82. package/dist/esm/src/hooks/index.js +1 -0
  83. package/dist/esm/src/hooks/useFloatingPosition.js +71 -0
  84. package/dist/esm/src/hooks/usePortalState.js +10 -0
  85. package/dist/esm/src/utils/dropdown/maxSelection.js +27 -0
  86. package/dist/esm/src/utils/dropdown/multiSelect.js +70 -14
  87. package/dist/temp/assets/scripts/featuredIcon.d.ts +22 -0
  88. package/dist/temp/assets/scripts/featuredIcon.js +79 -0
  89. package/dist/temp/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  90. package/dist/temp/assets/scripts/notification/FloatingNotification.js +156 -0
  91. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  92. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +111 -0
  93. package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +22 -0
  94. package/dist/temp/assets/scripts/notification/MessageNotification.js +140 -0
  95. package/dist/temp/assets/scripts/notification/Notification.d.ts +22 -0
  96. package/dist/temp/assets/scripts/notification/Notification.js +112 -0
  97. package/dist/temp/assets/scripts/notification/const/classNames.d.ts +43 -0
  98. package/dist/temp/assets/scripts/notification/const/classNames.js +44 -0
  99. package/dist/temp/assets/scripts/notification/const/icons.d.ts +25 -0
  100. package/dist/temp/assets/scripts/notification/const/icons.js +25 -0
  101. package/dist/temp/assets/scripts/notification/const/index.d.ts +5 -0
  102. package/dist/temp/assets/scripts/notification/const/index.js +4 -0
  103. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +32 -0
  104. package/dist/temp/assets/scripts/notification/const/sizes.js +40 -0
  105. package/dist/temp/assets/scripts/notification/const/types.d.ts +19 -0
  106. package/dist/temp/assets/scripts/notification/const/types.js +8 -0
  107. package/dist/temp/assets/scripts/notification/index.d.ts +8 -0
  108. package/dist/temp/assets/scripts/notification/index.js +10 -0
  109. package/dist/temp/assets/scripts/notification/positionSync.d.ts +50 -0
  110. package/dist/temp/assets/scripts/notification/positionSync.js +170 -0
  111. package/dist/temp/assets/scripts/notification/utils.d.ts +8 -0
  112. package/dist/temp/assets/scripts/notification/utils.js +115 -0
  113. package/dist/temp/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  114. package/dist/temp/assets/scripts/shared/ButtonCloseX.js +33 -0
  115. package/dist/temp/assets/scripts/utils/sanitize.d.ts +22 -0
  116. package/dist/temp/assets/scripts/utils/sanitize.js +31 -0
  117. package/dist/temp/src/components/data-display/data-grid/DataGrid.js +1 -1
  118. package/dist/temp/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  119. package/dist/temp/src/components/data-display/table/Table.d.ts +4 -1
  120. package/dist/temp/src/components/data-display/table/Table.js +53 -68
  121. package/dist/temp/src/components/data-display/table/types.d.ts +18 -0
  122. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  123. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +136 -0
  124. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  125. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +7 -11
  126. package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +1 -1
  127. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  128. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +30 -3
  129. package/dist/temp/src/components/index.d.ts +3 -0
  130. package/dist/temp/src/components/index.js +3 -0
  131. package/dist/temp/src/components/layout/block-container/BlockContainer.d.ts +19 -0
  132. package/dist/temp/src/components/layout/block-container/BlockContainer.js +11 -0
  133. package/dist/temp/src/components/layout/block-container/index.d.ts +1 -0
  134. package/dist/temp/src/components/layout/block-container/index.js +1 -0
  135. package/dist/temp/src/components/layout/block-header/BlockHeader.d.ts +23 -0
  136. package/dist/temp/src/components/layout/block-header/BlockHeader.js +21 -0
  137. package/dist/temp/src/components/layout/block-header/SubTitle.d.ts +19 -0
  138. package/dist/temp/src/components/layout/block-header/SubTitle.js +8 -0
  139. package/dist/temp/src/components/layout/block-header/index.d.ts +2 -0
  140. package/dist/temp/src/components/layout/block-header/index.js +2 -0
  141. package/dist/temp/src/components/layout/page-title/PageTitle.d.ts +22 -0
  142. package/dist/temp/src/components/layout/page-title/PageTitle.js +19 -0
  143. package/dist/temp/src/components/layout/page-title/index.d.ts +1 -0
  144. package/dist/temp/src/components/layout/page-title/index.js +1 -0
  145. package/dist/temp/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  146. package/dist/temp/src/components/overlays/dropdown/Dropdown.js +35 -11
  147. package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  148. package/dist/temp/src/components/overlays/notification/CalloutNotification.js +6 -0
  149. package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  150. package/dist/temp/src/components/overlays/notification/FloatingNotification.js +81 -13
  151. package/dist/temp/src/components/overlays/notification/Notification.d.ts +18 -3
  152. package/dist/temp/src/components/overlays/notification/Notification.js +4 -0
  153. package/dist/temp/src/components/overlays/notification/host.d.ts +9 -0
  154. package/dist/temp/src/components/overlays/notification/host.js +9 -0
  155. package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  156. package/dist/temp/src/components/overlays/tooltip/Tooltip.js +25 -22
  157. package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  158. package/dist/temp/src/components/select-dropdown/SelectDropdown.js +2 -2
  159. package/dist/temp/src/contexts/FloatingContext.d.ts +6 -0
  160. package/dist/temp/src/contexts/FloatingContext.js +4 -0
  161. package/dist/temp/src/contexts/index.d.ts +1 -0
  162. package/dist/temp/src/contexts/index.js +1 -0
  163. package/dist/temp/src/hooks/index.d.ts +1 -0
  164. package/dist/temp/src/hooks/index.js +1 -0
  165. package/dist/temp/src/hooks/useFloatingPosition.d.ts +19 -0
  166. package/dist/temp/src/hooks/useFloatingPosition.js +55 -0
  167. package/dist/temp/src/hooks/usePortalState.d.ts +6 -0
  168. package/dist/temp/src/hooks/usePortalState.js +7 -0
  169. package/dist/temp/src/utils/dropdown/maxSelection.d.ts +24 -0
  170. package/dist/temp/src/utils/dropdown/maxSelection.js +28 -0
  171. package/dist/temp/src/utils/dropdown/multiSelect.d.ts +42 -2
  172. package/dist/temp/src/utils/dropdown/multiSelect.js +66 -13
  173. package/dist/types/assets/scripts/featuredIcon.d.ts +22 -0
  174. package/dist/types/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  175. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  176. package/dist/types/assets/scripts/notification/MessageNotification.d.ts +22 -0
  177. package/dist/types/assets/scripts/notification/Notification.d.ts +22 -0
  178. package/dist/types/assets/scripts/notification/const/classNames.d.ts +43 -0
  179. package/dist/types/assets/scripts/notification/const/icons.d.ts +25 -0
  180. package/dist/types/assets/scripts/notification/const/index.d.ts +5 -0
  181. package/dist/types/assets/scripts/notification/const/sizes.d.ts +32 -0
  182. package/dist/types/assets/scripts/notification/const/types.d.ts +19 -0
  183. package/dist/types/assets/scripts/notification/index.d.ts +8 -0
  184. package/dist/types/assets/scripts/notification/positionSync.d.ts +50 -0
  185. package/dist/types/assets/scripts/notification/utils.d.ts +8 -0
  186. package/dist/types/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  187. package/dist/types/assets/scripts/utils/sanitize.d.ts +22 -0
  188. package/dist/types/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  189. package/dist/types/src/components/data-display/table/Table.d.ts +4 -1
  190. package/dist/types/src/components/data-display/table/types.d.ts +18 -0
  191. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  192. package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  193. package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  194. package/dist/types/src/components/index.d.ts +3 -0
  195. package/dist/types/src/components/layout/block-container/BlockContainer.d.ts +19 -0
  196. package/dist/types/src/components/layout/block-container/index.d.ts +1 -0
  197. package/dist/types/src/components/layout/block-header/BlockHeader.d.ts +23 -0
  198. package/dist/types/src/components/layout/block-header/SubTitle.d.ts +19 -0
  199. package/dist/types/src/components/layout/block-header/index.d.ts +2 -0
  200. package/dist/types/src/components/layout/page-title/PageTitle.d.ts +22 -0
  201. package/dist/types/src/components/layout/page-title/index.d.ts +1 -0
  202. package/dist/types/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  203. package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  204. package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  205. package/dist/types/src/components/overlays/notification/Notification.d.ts +18 -3
  206. package/dist/types/src/components/overlays/notification/host.d.ts +9 -0
  207. package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  208. package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  209. package/dist/types/src/contexts/FloatingContext.d.ts +6 -0
  210. package/dist/types/src/contexts/index.d.ts +1 -0
  211. package/dist/types/src/hooks/index.d.ts +1 -0
  212. package/dist/types/src/hooks/useFloatingPosition.d.ts +19 -0
  213. package/dist/types/src/hooks/usePortalState.d.ts +6 -0
  214. package/dist/types/src/utils/dropdown/maxSelection.d.ts +24 -0
  215. package/dist/types/src/utils/dropdown/multiSelect.d.ts +42 -2
  216. package/dist/ui-admin/assets/styles/style.css +596 -64
  217. package/package.json +1 -1
@@ -21,6 +21,14 @@ interface ComboBoxProps extends Omit<ComponentPropsWithRef<'div'>, 'size' | 'onC
21
21
  required?: boolean;
22
22
  multiple?: boolean;
23
23
  showFooterButtons?: boolean;
24
+ /**
25
+ * 최대 선택 가능 개수 (multiple=true에서만 유효).
26
+ * - `0` 이상의 정수: 제한 활성 → footer의 "전체선택" Link 미노출 (DES-SPEC-009 §3.10.1).
27
+ * - `0`은 모든 새 선택을 차단한다.
28
+ * - 양수는 도달 후 추가 선택을 무시한다 (이미 선택된 항목 해제는 정상).
29
+ * - 음수 / 비정수 / `null` / `undefined`: 제한 없음.
30
+ */
31
+ maxSelection?: number | null;
24
32
  onEdit?: () => void;
25
33
  }
26
34
  declare const ComboBox: import("react").ForwardRefExoticComponent<Omit<ComboBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -10,13 +10,6 @@ import { SelectDropdown } from '../../select-dropdown';
10
10
  import { HintText } from '../../shared/hintText/HintText';
11
11
  import { InputBase } from '../input-base/InputBase';
12
12
  const defaultMaxHeight = 275;
13
- const toggleMultiSelectValue = (currentValue, optionId) => {
14
- const currentValues = Array.isArray(currentValue) ? currentValue : [];
15
- if (currentValues.includes(optionId)) {
16
- return currentValues.filter((v) => v !== optionId);
17
- }
18
- return [...currentValues, optionId];
19
- };
20
13
  const notifyFormChange = (register, multiple, newValue) => {
21
14
  if (register?.onChange) {
22
15
  register.onChange({
@@ -27,7 +20,7 @@ const notifyFormChange = (register, multiple, newValue) => {
27
20
  });
28
21
  }
29
22
  };
30
- 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, 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, ...props }, ref) => {
31
24
  const internalRef = useRef(null);
32
25
  const dropdownRef = useRef(null);
33
26
  const inputRef = useRef(null);
@@ -38,7 +31,9 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
38
31
  if (disabled)
39
32
  return;
40
33
  if (multiple) {
41
- const newValue = toggleMultiSelectValue(value ?? [], option.id);
34
+ const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
35
+ if (newValue === null)
36
+ return;
42
37
  onChange?.(newValue);
43
38
  notifyFormChange(register, multiple, newValue);
44
39
  return;
@@ -103,6 +98,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
103
98
  // 나머지는 useDropdown 훅에서 처리
104
99
  dropdownHandleKeyDown(e);
105
100
  };
101
+ // biome-ignore lint/style/noNonNullAssertion: forwardRef 패턴에서 internalRef는 첫 렌더 후 항상 존재
106
102
  useImperativeHandle(ref, () => internalRef.current, []);
107
103
  const trailingElement = {
108
104
  type: 'custom',
@@ -116,7 +112,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
116
112
  color: 'gray300',
117
113
  };
118
114
  const currentSelectedValues = multiple && Array.isArray(value) ? value : [];
119
- const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, } = useMultiSelect(currentSelectedValues, optionItems);
115
+ const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, isMaxSelectionActive, tryToggle, } = useMultiSelect(currentSelectedValues, optionItems, { maxSelection });
120
116
  const handleSelectAll = () => {
121
117
  if (!multiple || !onChange)
122
118
  return;
@@ -160,7 +156,7 @@ const ComboBox = forwardRef(({ placeholder = '검색하세요', id, className, h
160
156
  }, className), ...props, children: [_jsxs("div", { className: "ncua-combobox__content", children: [_jsx(InputBase, { ref: inputRef, size: size, label: label, required: required, placeholder: placeholder, value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onClick: handleInputClick, disabled: disabled, destructive: destructive, leadingElement: leadingElement, trailingElement: trailingElement, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": comboboxId, "aria-disabled": disabled, autoComplete: "off", clearText: !!inputValue, onClearText: () => {
161
157
  setInputValue('');
162
158
  onSearch?.('');
163
- } }), _jsxs(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: displayedOptions, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: comboboxId, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, isKeyboardNavigation: isKeyboardNavigation, multiple: multiple, showFooterButtons: showFooter, selectAllButtonText: selectAllButtonText, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, componentType: "combobox", children: [showNoResults ? _jsx("li", { className: "ncua-select-dropdown__no-results", children: "\uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) : null, children] })] }), register && _jsx("input", { type: "hidden", ...register, value: hiddenInputValue }), hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), hasTags && (_jsx("div", { className: "ncua-combobox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) }))] }));
159
+ } }), _jsxs(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: displayedOptions, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: comboboxId, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, isKeyboardNavigation: isKeyboardNavigation, multiple: multiple, showFooterButtons: showFooter, selectAllButtonText: selectAllButtonText, showSelectAllAction: !isMaxSelectionActive, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, componentType: "combobox", children: [showNoResults ? _jsx("li", { className: "ncua-select-dropdown__no-results", children: "\uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) : null, children] })] }), register && _jsx("input", { type: "hidden", ...register, value: hiddenInputValue }), hintText && _jsx(HintText, { destructive: destructive, children: hintText })] }), hasTags && (_jsx("div", { className: "ncua-combobox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) }))] }));
164
160
  });
165
161
  ComboBox.displayName = 'ComboBox';
166
162
  export { defaultMaxHeight, ComboBox };
@@ -85,7 +85,7 @@ export const ImageFileInput = forwardRef(({ size = 'sm', accept, multiple = fals
85
85
  };
86
86
  const renderImagePreview = (files = []) => {
87
87
  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 }), isButtonHovered && !disabled && _jsx(Tooltip, { content: imagePreviewTooltipLabel, position: "bottom" })] }))] }));
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 })] }))] }));
89
89
  };
90
90
  const renderHintList = () => {
91
91
  if (!hintItems || hintItems.length === 0)
@@ -19,8 +19,21 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
19
19
  register?: UseFormRegisterReturn;
20
20
  maxHeight?: number;
21
21
  multiple?: boolean;
22
+ /**
23
+ * 최대 선택 가능 개수 (multiple=true에서만 유효).
24
+ * - `0` 이상의 정수: 제한 활성 → footer "전체선택" Link 미노출.
25
+ * - `0`은 모든 새 선택을 차단한다.
26
+ * - 양수는 도달 후 추가 선택을 무시한다 (이미 선택된 항목 해제는 정상).
27
+ * - 음수 / 비정수 / `null` / `undefined`: 제한 없음.
28
+ */
29
+ maxSelection?: number | null;
22
30
  onEdit?: () => void;
23
31
  align?: 'left' | 'right';
32
+ /**
33
+ * 옵션 패널을 React Portal로 body에 렌더한다.
34
+ * 미지정 시 FloatingContext.preferPortal 값을 따른다 (DataGrid.Table horizontalScroll 내부에서는 자동 true).
35
+ */
36
+ usePortal?: boolean;
24
37
  };
25
38
  declare const SelectBox: import("react").ForwardRefExoticComponent<Omit<SelectBoxProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
26
39
  export type { SelectBoxProps };
@@ -2,8 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { ChevronDown } from '@ncds/ui-admin-icon';
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
5
+ import { createPortal } from 'react-dom';
5
6
  import { COLOR } from '../../../../constant/color';
6
7
  import { useDropdown, useScrollLock } from '../../../hooks/dropdown';
8
+ import { useFloatingPosition } from '../../../hooks/useFloatingPosition';
9
+ import { usePortalState } from '../../../hooks/usePortalState';
7
10
  import { useMultiSelect } from '../../../utils/dropdown/multiSelect';
8
11
  import { Tag } from '../../feedback-and-status/tag';
9
12
  import { SelectDropdown } from '../../select-dropdown';
@@ -37,9 +40,12 @@ function DisplayValue({ displayValue }) {
37
40
  }
38
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 })] }));
39
42
  }
40
- 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, align = 'left', 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, ...props }, ref
44
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 옵션/멀티/태그/포탈 등 필수 분기 통합
45
+ ) => {
41
46
  const internalRef = useRef(null);
42
47
  const dropdownRef = useRef(null);
48
+ const { shouldPortal, portalContainer } = usePortalState(usePortal);
43
49
  const [selectedTags, setSelectedTags] = useState([]);
44
50
  const selectedOption = useMemo(() => {
45
51
  if (multiple)
@@ -55,6 +61,14 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
55
61
  const handleOptionSelect = (option) => {
56
62
  if (disabled)
57
63
  return;
64
+ if (multiple) {
65
+ const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
66
+ if (newValue === null)
67
+ return;
68
+ onChange?.(newValue);
69
+ notifyRegister(register, newValue, multiple);
70
+ return;
71
+ }
58
72
  const newValue = computeNewValue(option, value, multiple);
59
73
  onChange?.(newValue);
60
74
  notifyRegister(register, newValue, multiple);
@@ -82,7 +96,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
82
96
  }, [handleMouseMove, setFocusedIndex]);
83
97
  // Multiple select 관련 로직
84
98
  const currentSelectedValues = multiple && Array.isArray(value) ? value : [];
85
- const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, } = useMultiSelect(currentSelectedValues, optionItems);
99
+ const { buttonText: selectAllButtonText, toggleSelectAll, getSelectedTagsData, removeTag, isMaxSelectionActive, tryToggle, } = useMultiSelect(currentSelectedValues, optionItems, { maxSelection });
86
100
  const handleSelectAll = () => {
87
101
  if (!multiple || !onChange)
88
102
  return;
@@ -109,13 +123,26 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
109
123
  }
110
124
  };
111
125
  useScrollLock(isOpen, dropdownRef);
126
+ // biome-ignore lint/correctness/useExhaustiveDependencies: optionItems 변경 시 너비 재계산 필요
112
127
  useLayoutEffect(() => {
113
128
  if (autoWidth && isOpen && dropdownRef.current && internalRef.current) {
114
129
  const dropdownWidth = dropdownRef.current.offsetWidth;
115
130
  internalRef.current.style.width = `${dropdownWidth}px`;
116
131
  }
117
132
  }, [autoWidth, isOpen, optionItems]);
133
+ const floatingStyle = useFloatingPosition({
134
+ enabled: shouldPortal,
135
+ isOpen,
136
+ triggerRef: internalRef,
137
+ floatingRef: dropdownRef,
138
+ direction: dropdownDirection,
139
+ align,
140
+ matchTriggerWidth: true,
141
+ });
142
+ // biome-ignore lint/style/noNonNullAssertion: forwardRef 패턴에서 internalRef는 항상 존재
118
143
  useImperativeHandle(ref, () => internalRef.current, []);
144
+ const selectDropdownNode = (_jsx(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: optionItems, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: `selectbox-options-${id || 'default'}`, multiple: multiple, showFooterButtons: multiple, selectAllButtonText: selectAllButtonText, showSelectAllAction: !isMaxSelectionActive, componentType: "selectbox", isKeyboardNavigation: isKeyboardNavigation, activeDescendantId: activeDescendantId, align: align, className: shouldPortal ? 'ncua-select-dropdown--portal' : undefined, style: shouldPortal && floatingStyle ? floatingStyle : undefined, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, onOptionHover: handleOptionHover, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, children: children }));
145
+ const portaledDropdown = shouldPortal && portalContainer && isOpen ? createPortal(selectDropdownNode, portalContainer) : null;
119
146
  return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: internalRef, className: classNames('ncua-selectbox', `ncua-selectbox--${size}`, {
120
147
  'ncua-selectbox--open': isOpen,
121
148
  'ncua-selectbox--disabled': disabled,
@@ -124,7 +151,7 @@ const SelectBox = forwardRef(({ placeholder = '선택하세요', disabledPlaceho
124
151
  destructive: destructive,
125
152
  }, className), ...props, children: [_jsxs("div", { className: classNames('ncua-selectbox__content'), onClick: toggleDropdown, onKeyDown: handleKeyDown, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": `selectbox-options-${id || 'default'}`, "aria-disabled": disabled, "aria-label": selectedOption ? selectedOption.label : placeholder, "aria-activedescendant": activeDescendantId, children: [_jsxs("div", { className: "ncua-selectbox__content-inner", children: [_jsx("div", { className: "ncua-selectbox__value", children: _jsx(DisplayValue, { displayValue: displayValue }) }), _jsx(ChevronDown, { width: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, height: size === 'xs' ? ICON_SIZE_XS : ICON_SIZE_SM, color: disabled ? COLOR.gray300 : COLOR.gray500, className: classNames('ncua-selectbox__arrow', {
126
153
  'ncua-selectbox__arrow--up': isOpen,
127
- }) })] }), _jsx(SelectDropdown, { ref: dropdownRef, isOpen: isOpen, direction: dropdownDirection, size: size, options: optionItems, value: value, focusedIndex: focusedIndex, maxHeight: maxHeight, listboxId: `selectbox-options-${id || 'default'}`, multiple: multiple, showFooterButtons: multiple, selectAllButtonText: selectAllButtonText, componentType: "selectbox", isKeyboardNavigation: isKeyboardNavigation, activeDescendantId: activeDescendantId, align: align, onOptionSelect: handleDropdownSelect, onMouseMove: handleMouseMove, onOptionHover: handleOptionHover, onSelectAll: handleSelectAll, onEdit: handleEdit, onComplete: handleComplete, children: children })] }), hintText && (_jsx(HintText, { destructive: destructive, className: "ncua-hint-text", children: hintText })), register && (_jsx("input", { type: "hidden", ...register, value: multiple ? JSON.stringify(value || []) : String(value || '') }))] }), selectedTags.length > 0 && (_jsx("div", { className: "ncua-selectbox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) }))] }));
154
+ }) })] }), !shouldPortal && selectDropdownNode] }), hintText && (_jsx(HintText, { destructive: destructive, className: "ncua-hint-text", children: hintText })), register && (_jsx("input", { type: "hidden", ...register, value: multiple ? JSON.stringify(value || []) : String(value || '') }))] }), selectedTags.length > 0 && (_jsx("div", { className: "ncua-selectbox__tags", children: selectedTags.map((tag) => (_jsx(Tag, { text: tag.label, size: "sm", close: true, onButtonClick: () => handleRemoveTag(tag.id) }, tag.id))) })), portaledDropdown] }));
128
155
  });
129
156
  SelectBox.displayName = 'SelectBox';
130
157
  export { DEFAULT_MAX_HEIGHT, SelectBox };
@@ -31,7 +31,10 @@ export * from './forms-and-input/textarea';
31
31
  export * from './forms-and-input/toggle';
32
32
  export * from './image-and-icons/dot';
33
33
  export * from './image-and-icons/featured-icon';
34
+ export * from './layout/block-container';
35
+ export * from './layout/block-header';
34
36
  export * from './layout/divider';
37
+ export * from './layout/page-title';
35
38
  export * from './navigation/bread-crumb';
36
39
  export * from './navigation/horizontal-tab';
37
40
  export * from './navigation/pagination';
@@ -36,7 +36,10 @@ export * from './forms-and-input/toggle';
36
36
  export * from './image-and-icons/dot';
37
37
  export * from './image-and-icons/featured-icon';
38
38
  // Layout
39
+ export * from './layout/block-container';
40
+ export * from './layout/block-header';
39
41
  export * from './layout/divider';
42
+ export * from './layout/page-title';
40
43
  // Navigation
41
44
  export * from './navigation/bread-crumb';
42
45
  export * from './navigation/horizontal-tab';
@@ -0,0 +1,19 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ interface BlockContainerProps extends ComponentPropsWithoutRef<'section'> {
3
+ children: ReactNode;
4
+ className?: string;
5
+ }
6
+ interface BlockContainerBodyProps extends ComponentPropsWithoutRef<'div'> {
7
+ children: ReactNode;
8
+ className?: string;
9
+ }
10
+ export declare const BlockContainer: {
11
+ ({ children, className, ...rest }: BlockContainerProps): import("react/jsx-runtime").JSX.Element;
12
+ displayName: string;
13
+ } & {
14
+ Body: {
15
+ ({ children, className, ...rest }: BlockContainerBodyProps): import("react/jsx-runtime").JSX.Element;
16
+ displayName: string;
17
+ };
18
+ };
19
+ export type { BlockContainerProps, BlockContainerBodyProps };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import classNames from 'classnames';
3
+ const BlockContainerBase = ({ children, className, ...rest }) => {
4
+ return (_jsx("section", { className: classNames('ncua-block-container', className), ...rest, children: children }));
5
+ };
6
+ BlockContainerBase.displayName = 'BlockContainer';
7
+ const Body = ({ children, className, ...rest }) => {
8
+ return (_jsx("div", { className: classNames('ncua-block-container__body', className), ...rest, children: children }));
9
+ };
10
+ Body.displayName = 'BlockContainer.Body';
11
+ export const BlockContainer = Object.assign(BlockContainerBase, { Body });
@@ -0,0 +1 @@
1
+ export * from './BlockContainer';
@@ -0,0 +1 @@
1
+ export * from './BlockContainer';
@@ -0,0 +1,23 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ interface BlockHeaderProps extends Omit<ComponentPropsWithoutRef<'header'>, 'title' | 'children'> {
3
+ title: ReactNode;
4
+ tooltip?: string;
5
+ action?: ReactNode;
6
+ showDivider?: boolean;
7
+ badge?: ReactNode;
8
+ description?: string;
9
+ collapsible?: {
10
+ expanded: boolean;
11
+ onToggle: () => void;
12
+ };
13
+ showRequiredNotice?: boolean;
14
+ controlStrip?: ReactNode;
15
+ children?: ReactNode;
16
+ className?: string;
17
+ }
18
+ declare const BlockHeader: {
19
+ ({ title, tooltip, action, showDivider, badge, description, collapsible, showRequiredNotice, controlStrip, children, className, ...rest }: BlockHeaderProps): import("react/jsx-runtime").JSX.Element;
20
+ displayName: string;
21
+ };
22
+ export { BlockHeader };
23
+ export type { BlockHeaderProps };
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, ChevronUp } from '@ncds/ui-admin-icon';
3
+ import classNames from 'classnames';
4
+ import { Tooltip } from '../../overlays/tooltip/Tooltip';
5
+ const ICON_SIZE = 24;
6
+ const CollapsibleButton = ({ expanded, onToggle }) => (_jsx("button", { type: "button", className: "ncua-block-header__collapsible-btn", onClick: onToggle, "aria-expanded": expanded, children: expanded ? (_jsx(ChevronUp, { width: ICON_SIZE, height: ICON_SIZE, color: "var(--gray-300)" })) : (_jsx(ChevronDown, { width: ICON_SIZE, height: ICON_SIZE, color: "var(--gray-300)" })) }));
7
+ const BlockHeader = ({ title, tooltip, action, showDivider = true, badge, description, collapsible, showRequiredNotice = false, controlStrip, children, className, ...rest }) => {
8
+ const hasColumnLayout = !!description;
9
+ const hasTabChildren = !!children;
10
+ const hasControlStrip = !!controlStrip;
11
+ const isCollapsed = !!collapsible && !collapsible.expanded;
12
+ return (_jsxs("header", { className: classNames('ncua-block-header', {
13
+ 'ncua-block-header--column': hasColumnLayout,
14
+ 'ncua-block-header--no-divider': !showDivider || hasTabChildren || isCollapsed,
15
+ 'ncua-block-header--has-tab': hasTabChildren && !isCollapsed,
16
+ 'ncua-block-header--has-control-strip': hasControlStrip,
17
+ 'ncua-block-header--is-required': showRequiredNotice,
18
+ }, className), ...rest, children: [_jsxs("div", { className: "ncua-block-header__row", children: [_jsxs("div", { className: "ncua-block-header__title-area", children: [_jsx("span", { className: "ncua-block-header__title", children: title }), tooltip && _jsx(Tooltip, { content: tooltip, size: "sm", position: "top", hideArrow: true, iconType: "fill" }), badge && _jsx("span", { className: "ncua-block-header__badge", children: badge })] }), _jsxs("div", { className: "ncua-block-header__action-area", children: [showRequiredNotice && (_jsxs("span", { className: "ncua-block-header__required-notice", children: [_jsx("span", { className: "ncua-block-header__required-notice--red", children: "* \uB294 \uD544\uC218 \uC785\uB825" }), _jsx("span", { className: "ncua-block-header__required-notice--gray", children: " \uD56D\uBAA9\uC785\uB2C8\uB2E4." })] })), action, collapsible && _jsx(CollapsibleButton, { ...collapsible })] })] }), hasColumnLayout && _jsx("p", { className: "ncua-block-header__description", children: description }), hasControlStrip && _jsx("div", { className: "ncua-block-header__control-strip", children: controlStrip }), hasTabChildren && !isCollapsed && _jsx("div", { className: "ncua-block-header__tabs", children: children })] }));
19
+ };
20
+ BlockHeader.displayName = 'BlockHeader';
21
+ export { BlockHeader };
@@ -0,0 +1,19 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ import type { Size } from '../../../../constant/size';
3
+ type SubTitleSize = 'middle' | Extract<Size, 'sm' | 'xs'>;
4
+ interface SubTitleProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
5
+ title: ReactNode;
6
+ size?: SubTitleSize;
7
+ tooltip?: string;
8
+ description?: string;
9
+ error?: string;
10
+ action?: ReactNode;
11
+ required?: boolean;
12
+ className?: string;
13
+ }
14
+ declare const SubTitle: {
15
+ ({ title, size, tooltip, description, error, action, required, className, ...rest }: SubTitleProps): import("react/jsx-runtime").JSX.Element;
16
+ displayName: string;
17
+ };
18
+ export { SubTitle };
19
+ export type { SubTitleProps, SubTitleSize };
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import classNames from 'classnames';
3
+ import { Tooltip } from '../../overlays/tooltip/Tooltip';
4
+ const SubTitle = ({ title, size = 'sm', tooltip, description, error, action, required = false, className, ...rest }) => {
5
+ return (_jsxs("div", { className: classNames('ncua-sub-title', `ncua-sub-title--${size}`, className), ...rest, children: [_jsxs("div", { className: "ncua-sub-title__title-row", children: [_jsxs("div", { className: "ncua-sub-title__title-area", children: [required && _jsx("span", { className: "ncua-sub-title__required-marker", children: "*" }), _jsx("span", { className: "ncua-sub-title__title", children: title }), tooltip && _jsx(Tooltip, { content: tooltip, size: "sm", position: "right", hideArrow: true })] }), action && _jsx("div", { className: "ncua-sub-title__action", children: action })] }), description && _jsx("p", { className: "ncua-sub-title__description", children: description }), error && _jsx("p", { className: "ncua-sub-title__error", children: error })] }));
6
+ };
7
+ SubTitle.displayName = 'BlockHeader.SubTitle';
8
+ export { SubTitle };
@@ -0,0 +1,2 @@
1
+ export * from './BlockHeader';
2
+ export * from './SubTitle';
@@ -0,0 +1,2 @@
1
+ export * from './BlockHeader';
2
+ export * from './SubTitle';
@@ -0,0 +1,22 @@
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
+ type PageTitleVariant = 'default' | 'detail' | 'fixed' | 'fixed-detail';
3
+ interface PageTitleBreadcrumbItem {
4
+ label: string;
5
+ href?: string;
6
+ }
7
+ interface PageTitleProps extends ComponentPropsWithoutRef<'header'> {
8
+ title: string;
9
+ variant?: PageTitleVariant;
10
+ breadcrumbItems?: PageTitleBreadcrumbItem[];
11
+ onBack?: () => void;
12
+ guideButton?: ReactNode;
13
+ primaryAction?: ReactNode;
14
+ secondaryAction?: ReactNode;
15
+ className?: string;
16
+ }
17
+ declare const PageTitle: {
18
+ ({ title, variant, breadcrumbItems, onBack, guideButton, primaryAction, secondaryAction, className, ...rest }: PageTitleProps): import("react/jsx-runtime").JSX.Element;
19
+ displayName: string;
20
+ };
21
+ export { PageTitle };
22
+ export type { PageTitleProps, PageTitleVariant, PageTitleBreadcrumbItem };
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronLeft, ChevronRight } from '@ncds/ui-admin-icon';
3
+ import classNames from 'classnames';
4
+ import { Fragment } from 'react';
5
+ import { Button } from '../../action/button/Button';
6
+ const renderBreadcrumb = (items) => (_jsx("nav", { className: "ncua-page-title__breadcrumb", "aria-label": "breadcrumb", children: items.map((item, i) => (
7
+ // biome-ignore lint/suspicious/noArrayIndexKey: breadcrumb items may have duplicate labels
8
+ _jsx(Fragment, { children: i < items.length - 1 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "ncua-page-title__breadcrumb-item", children: item.href ? _jsx("a", { href: item.href, children: item.label }) : item.label }), _jsx(ChevronRight, { className: "ncua-page-title__breadcrumb-separator" })] })) : (_jsx("span", { className: "ncua-page-title__breadcrumb-current", children: item.label })) }, i))) }));
9
+ const renderBackButton = (onBack) => (_jsx(Button, { label: "", hierarchy: "secondary-gray", size: "xs", onlyIcon: true, leadingIcon: { type: 'icon', icon: ChevronLeft }, onClick: onBack, "aria-label": "\uC774\uC804 \uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uAC00\uAE30", className: "ncua-page-title__back-btn" }));
10
+ const PageTitle = ({ title, variant = 'default', breadcrumbItems, onBack, guideButton, primaryAction, secondaryAction, className, ...rest }) => {
11
+ const isFixed = variant === 'fixed' || variant === 'fixed-detail';
12
+ const isDetail = variant === 'detail' || variant === 'fixed-detail';
13
+ const hasBreadcrumb = !isFixed && breadcrumbItems && breadcrumbItems.length > 0;
14
+ return (_jsx("header", { className: classNames('ncua-page-title', { 'ncua-page-title--fixed': isFixed }, className), ...rest, children: _jsx("div", { className: "ncua-page-title__page-header", children: _jsxs("div", { className: classNames('ncua-page-title__header', {
15
+ 'ncua-page-title__header--has-breadcrumb': hasBreadcrumb,
16
+ }), children: [_jsxs("div", { className: "ncua-page-title__container", children: [hasBreadcrumb && renderBreadcrumb(breadcrumbItems), _jsxs("div", { className: "ncua-page-title__title-row", children: [isDetail && onBack && renderBackButton(onBack), _jsx("h1", { className: "ncua-page-title__title", children: title }), guideButton && _jsx("div", { className: "ncua-page-title__guide-btn", children: guideButton })] })] }), (secondaryAction || primaryAction) && (_jsxs("div", { className: "ncua-page-title__actions", children: [secondaryAction, primaryAction] }))] }) }) }));
17
+ };
18
+ PageTitle.displayName = 'PageTitle';
19
+ export { PageTitle };
@@ -0,0 +1 @@
1
+ export * from './PageTitle';
@@ -0,0 +1 @@
1
+ export * from './PageTitle';
@@ -49,6 +49,11 @@ export type DropdownBaseProps = {
49
49
  className?: string;
50
50
  opened?: boolean;
51
51
  closeOnClickOutside?: boolean;
52
+ /**
53
+ * 메뉴를 React Portal로 body에 렌더한다.
54
+ * 미지정 시 FloatingContext.preferPortal 값을 따른다 (DataGrid.Table horizontalScroll 내부에서는 자동 true).
55
+ */
56
+ usePortal?: boolean;
52
57
  };
53
58
  export type ActionDropdownProps = DropdownBaseProps & {
54
59
  variant?: 'action';
@@ -7,6 +7,9 @@ import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-sc
7
7
  import { attachClosestEdge, extractClosestEdge, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
8
8
  import { DotsGrid02, DotsVertical, Eye, EyeOff } from '@ncds/ui-admin-icon';
9
9
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
10
+ import { createPortal } from 'react-dom';
11
+ import { useFloatingPosition } from '../../../hooks/useFloatingPosition';
12
+ import { usePortalState } from '../../../hooks/usePortalState';
10
13
  import { Button } from '../../action/button';
11
14
  import { applyDraftToItems, arrayReorderByEdge, hasDraftChanged, initDraftState } from './utils';
12
15
  const DROPDOWN_ID_RADIX = 36;
@@ -82,14 +85,25 @@ const SortableConfigItem = ({ item, isVisible, showVisibilityToggle, sortable, d
82
85
  }
83
86
  }, children: _jsx(DotsGrid02, {}) })), _jsx("div", { className: "ncua-dropdown__item-content", children: _jsxs("div", { className: "ncua-dropdown__item-text-group", children: [ItemIcon && _jsx(ItemIcon, { className: "ncua-dropdown__item-icon" }), _jsx("span", { className: "ncua-dropdown__item-text", children: item.text })] }) }), showVisibilityToggle && (_jsx("button", { type: "button", className: "ncua-dropdown__item-visibility", "aria-label": `${item.text} 노출`, "aria-pressed": isVisible, onClick: () => onToggleVisibility(item.id), children: isVisible ? _jsx(Eye, {}) : _jsx(EyeOff, {}) }))] }));
84
87
  };
88
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 액션/설정 두 variant 통합 + drag-and-drop 필수 분기
85
89
  export const Dropdown = (props) => {
86
- const { trigger, align = 'left', header, groups, className, opened = false, closeOnClickOutside = true } = props;
90
+ const { trigger, align = 'left', header, groups, className, opened = false, closeOnClickOutside = true, usePortal, } = props;
87
91
  const variant = props.variant ?? 'action';
88
92
  const closeOnClickItem = variant === 'action' ? (props.closeOnClickItem ?? true) : false;
89
93
  const [isOpen, setIsOpen] = useState(opened);
90
94
  const dropdownRef = useRef(null);
91
95
  const triggerRef = useRef(null);
96
+ const menuRef = useRef(null);
92
97
  const menuItemsRef = useRef(null);
98
+ const { shouldPortal, portalContainer } = usePortalState(usePortal);
99
+ const floatingStyle = useFloatingPosition({
100
+ enabled: shouldPortal,
101
+ isOpen,
102
+ triggerRef,
103
+ floatingRef: menuRef,
104
+ direction: 'down',
105
+ align,
106
+ });
93
107
  const dropdownIdRef = useRef(`ncua-dropdown-${Math.random().toString(DROPDOWN_ID_RADIX).slice(DROPDOWN_ID_SLICE_START, DROPDOWN_ID_SLICE_END)}`);
94
108
  const dropdownId = dropdownIdRef.current;
95
109
  useEffect(() => {
@@ -144,7 +158,10 @@ export const Dropdown = (props) => {
144
158
  triggerRef.current?.focus();
145
159
  };
146
160
  const handleClickOutside = (event) => {
147
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
161
+ const target = event.target;
162
+ const insideContainer = dropdownRef.current?.contains(target) ?? false;
163
+ const insidePortaledMenu = menuRef.current?.contains(target) ?? false;
164
+ if (!insideContainer && !insidePortaledMenu) {
148
165
  setIsOpen(false);
149
166
  }
150
167
  };
@@ -156,6 +173,7 @@ export const Dropdown = (props) => {
156
173
  }
157
174
  }
158
175
  };
176
+ // biome-ignore lint/correctness/useExhaustiveDependencies: handleClickOutside는 안정적 참조
159
177
  useEffect(() => {
160
178
  if (closeOnClickOutside) {
161
179
  document.addEventListener('mousedown', handleClickOutside);
@@ -165,6 +183,7 @@ export const Dropdown = (props) => {
165
183
  }
166
184
  }, [closeOnClickOutside]);
167
185
  // ESC 키로 닫고 trigger로 포커스 복귀
186
+ // biome-ignore lint/correctness/useExhaustiveDependencies: closeAndRestoreFocus는 안정적 참조
168
187
  useEffect(() => {
169
188
  if (!isOpen)
170
189
  return;
@@ -261,13 +280,18 @@ export const Dropdown = (props) => {
261
280
  const dropdownClasses = ['ncua-dropdown', className, align === 'right' ? 'ncua-dropdown--right' : '']
262
281
  .filter(Boolean)
263
282
  .join(' ');
264
- return (_jsxs("div", { className: dropdownClasses, ref: dropdownRef, children: [renderTrigger(), isOpen && (_jsxs("div", { className: "ncua-dropdown__menu", role: variant === 'config' ? 'dialog' : 'menu', "aria-label": variant === 'config' ? '설정' : undefined, children: [renderHeader(), _jsx("div", { ref: menuItemsRef, className: "ncua-dropdown__menu-items", children: groups.map((group) => {
265
- // config variant uses draft.order to drive the rendered order
266
- const orderedItems = variant === 'config' && draft
267
- ? draft.order
268
- .map((id) => group.items.find((i) => i.id === id))
269
- .filter((i) => i !== undefined)
270
- : group.items;
271
- return (_jsx("div", { className: "ncua-dropdown__group", children: orderedItems.map((item) => variant === 'config' ? renderConfigItem(item, group.sortable === true) : renderActionItem(item)) }, group.items[0]?.id));
272
- }) }), renderFooter()] }))] }));
283
+ const menuClasses = ['ncua-dropdown__menu', shouldPortal ? 'ncua-dropdown__menu--portal' : '']
284
+ .filter(Boolean)
285
+ .join(' ');
286
+ const menuNode = isOpen ? (_jsxs("div", { ref: menuRef, className: menuClasses, role: variant === 'config' ? 'dialog' : 'menu', "aria-label": variant === 'config' ? '설정' : undefined, style: shouldPortal && floatingStyle ? floatingStyle : undefined, children: [renderHeader(), _jsx("div", { ref: menuItemsRef, className: "ncua-dropdown__menu-items", children: groups.map((group) => {
287
+ // config variant uses draft.order to drive the rendered order
288
+ const orderedItems = variant === 'config' && draft
289
+ ? draft.order
290
+ .map((id) => group.items.find((i) => i.id === id))
291
+ .filter((i) => i !== undefined)
292
+ : group.items;
293
+ return (_jsx("div", { className: "ncua-dropdown__group", children: orderedItems.map((item) => variant === 'config' ? renderConfigItem(item, group.sortable === true) : renderActionItem(item)) }, group.items[0]?.id));
294
+ }) }), renderFooter()] })) : null;
295
+ const portaledMenu = shouldPortal && portalContainer && menuNode ? createPortal(menuNode, portalContainer) : null;
296
+ return (_jsxs("div", { className: dropdownClasses, ref: dropdownRef, children: [renderTrigger(), !shouldPortal && menuNode, portaledMenu] }));
273
297
  };
@@ -0,0 +1,9 @@
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
+ import type { NotificationColor } from './Notification';
3
+ interface CalloutNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
4
+ color?: NotificationColor;
5
+ title?: ReactNode;
6
+ }
7
+ declare const CalloutNotification: import("react").ForwardRefExoticComponent<CalloutNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
8
+ export type { CalloutNotificationProps };
9
+ export { CalloutNotification };
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import classNames from 'classnames';
3
+ import { forwardRef } from 'react';
4
+ const CalloutNotification = forwardRef(({ color = 'neutral', className, title, ...rest }, ref) => (_jsx("div", { ref: ref, className: classNames('ncua-callout-notification', `ncua-callout-notification--${color}`, className), ...rest, children: title })));
5
+ CalloutNotification.displayName = 'CalloutNotification';
6
+ export { CalloutNotification };
@@ -31,6 +31,21 @@ interface FloatingNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>
31
31
  * @default 0
32
32
  */
33
33
  autoClose?: number;
34
+ /**
35
+ * Portal 마운트 여부.
36
+ *
37
+ * - `false` (기본값): 부모 JSX 트리 안에 카드로 그대로 렌더된다.
38
+ * 특정 컨테이너 안에서 표시하거나, 인라인 미리보기 (docs/스토리북) 용도.
39
+ *
40
+ * - `true`: document.body의 `.ncua-floating-notification-host` 싱글톤에
41
+ * `createPortal`로 마운트되어 우측 상단에 노출. 다중 발생 시 최신 토스트가
42
+ * 상단에 노출되고 이전 토스트들이 아래로 12px씩 겹쳐 쌓인다 (LIFO,
43
+ * `--ncua-floating-notification-stack-overlap` 변수로 조정 가능).
44
+ * Vanilla(CDN) 측 `new ncua.Notification({type:'floating'}).show()` 와 동일한 동작.
45
+ *
46
+ * @default false
47
+ */
48
+ portal?: boolean;
34
49
  }
35
50
  declare const FloatingNotification: import("react").ForwardRefExoticComponent<FloatingNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
36
51
  export type { FloatingNotificationProps };