@ncds/ui-admin 1.8.4 → 1.8.6

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 (187) 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/forms-and-input/slider/Slider.js +2 -3
  23. package/dist/cjs/src/components/overlays/dropdown/Dropdown.js +47 -19
  24. package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +25 -0
  25. package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +86 -13
  26. package/dist/cjs/src/components/overlays/notification/Notification.js +7 -0
  27. package/dist/cjs/src/components/overlays/notification/host.js +12 -0
  28. package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +57 -44
  29. package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +2 -1
  30. package/dist/cjs/src/contexts/FloatingContext.js +11 -0
  31. package/dist/cjs/src/contexts/index.js +16 -0
  32. package/dist/cjs/src/hooks/index.js +11 -0
  33. package/dist/cjs/src/hooks/useFloatingPosition.js +78 -0
  34. package/dist/cjs/src/hooks/usePortalState.js +17 -0
  35. package/dist/cjs/src/types/component-meta.js +8 -1
  36. package/dist/cjs/src/utils/dropdown/maxSelection.js +35 -0
  37. package/dist/cjs/src/utils/dropdown/multiSelect.js +72 -15
  38. package/dist/esm/assets/scripts/featuredIcon.js +80 -0
  39. package/dist/esm/assets/scripts/notification/FloatingNotification.js +171 -0
  40. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +126 -0
  41. package/dist/esm/assets/scripts/notification/MessageNotification.js +152 -0
  42. package/dist/esm/assets/scripts/notification/Notification.js +113 -0
  43. package/dist/esm/assets/scripts/notification/const/classNames.js +44 -0
  44. package/dist/esm/assets/scripts/notification/const/icons.js +25 -0
  45. package/dist/esm/assets/scripts/notification/const/index.js +4 -0
  46. package/dist/esm/assets/scripts/notification/const/sizes.js +40 -0
  47. package/dist/esm/assets/scripts/notification/const/types.js +8 -0
  48. package/dist/esm/assets/scripts/notification/index.js +10 -0
  49. package/dist/esm/assets/scripts/notification/positionSync.js +171 -0
  50. package/dist/esm/assets/scripts/notification/utils.js +109 -0
  51. package/dist/esm/assets/scripts/shared/ButtonCloseX.js +37 -0
  52. package/dist/esm/assets/scripts/utils/sanitize.js +31 -0
  53. package/dist/esm/src/components/data-display/data-grid/DataGrid.js +5 -1
  54. package/dist/esm/src/components/data-display/table/Table.js +118 -96
  55. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +179 -0
  56. package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
  57. package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
  58. package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +67 -29
  59. package/dist/esm/src/components/forms-and-input/slider/Slider.js +1 -2
  60. package/dist/esm/src/components/overlays/dropdown/Dropdown.js +47 -19
  61. package/dist/esm/src/components/overlays/notification/CalloutNotification.js +19 -0
  62. package/dist/esm/src/components/overlays/notification/FloatingNotification.js +86 -14
  63. package/dist/esm/src/components/overlays/notification/Notification.js +7 -0
  64. package/dist/esm/src/components/overlays/notification/host.js +9 -0
  65. package/dist/esm/src/components/overlays/tooltip/Tooltip.js +58 -45
  66. package/dist/esm/src/components/select-dropdown/SelectDropdown.js +2 -1
  67. package/dist/esm/src/contexts/FloatingContext.js +4 -0
  68. package/dist/esm/src/contexts/index.js +1 -0
  69. package/dist/esm/src/hooks/index.js +1 -0
  70. package/dist/esm/src/hooks/useFloatingPosition.js +71 -0
  71. package/dist/esm/src/hooks/usePortalState.js +10 -0
  72. package/dist/esm/src/types/component-meta.js +5 -1
  73. package/dist/esm/src/utils/dropdown/maxSelection.js +27 -0
  74. package/dist/esm/src/utils/dropdown/multiSelect.js +70 -14
  75. package/dist/temp/assets/scripts/featuredIcon.d.ts +22 -0
  76. package/dist/temp/assets/scripts/featuredIcon.js +79 -0
  77. package/dist/temp/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  78. package/dist/temp/assets/scripts/notification/FloatingNotification.js +156 -0
  79. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  80. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +111 -0
  81. package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +22 -0
  82. package/dist/temp/assets/scripts/notification/MessageNotification.js +140 -0
  83. package/dist/temp/assets/scripts/notification/Notification.d.ts +22 -0
  84. package/dist/temp/assets/scripts/notification/Notification.js +112 -0
  85. package/dist/temp/assets/scripts/notification/const/classNames.d.ts +43 -0
  86. package/dist/temp/assets/scripts/notification/const/classNames.js +44 -0
  87. package/dist/temp/assets/scripts/notification/const/icons.d.ts +25 -0
  88. package/dist/temp/assets/scripts/notification/const/icons.js +25 -0
  89. package/dist/temp/assets/scripts/notification/const/index.d.ts +5 -0
  90. package/dist/temp/assets/scripts/notification/const/index.js +4 -0
  91. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +32 -0
  92. package/dist/temp/assets/scripts/notification/const/sizes.js +40 -0
  93. package/dist/temp/assets/scripts/notification/const/types.d.ts +19 -0
  94. package/dist/temp/assets/scripts/notification/const/types.js +8 -0
  95. package/dist/temp/assets/scripts/notification/index.d.ts +8 -0
  96. package/dist/temp/assets/scripts/notification/index.js +10 -0
  97. package/dist/temp/assets/scripts/notification/positionSync.d.ts +50 -0
  98. package/dist/temp/assets/scripts/notification/positionSync.js +170 -0
  99. package/dist/temp/assets/scripts/notification/utils.d.ts +8 -0
  100. package/dist/temp/assets/scripts/notification/utils.js +115 -0
  101. package/dist/temp/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  102. package/dist/temp/assets/scripts/shared/ButtonCloseX.js +33 -0
  103. package/dist/temp/assets/scripts/utils/sanitize.d.ts +22 -0
  104. package/dist/temp/assets/scripts/utils/sanitize.js +31 -0
  105. package/dist/temp/src/components/data-display/data-grid/DataGrid.js +1 -1
  106. package/dist/temp/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  107. package/dist/temp/src/components/data-display/table/Table.d.ts +4 -1
  108. package/dist/temp/src/components/data-display/table/Table.js +53 -68
  109. package/dist/temp/src/components/data-display/table/types.d.ts +18 -0
  110. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  111. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +136 -0
  112. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  113. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +7 -11
  114. package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +1 -1
  115. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  116. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +30 -3
  117. package/dist/temp/src/components/forms-and-input/slider/Slider.d.ts +0 -1
  118. package/dist/temp/src/components/forms-and-input/slider/Slider.js +0 -1
  119. package/dist/temp/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  120. package/dist/temp/src/components/overlays/dropdown/Dropdown.js +35 -11
  121. package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  122. package/dist/temp/src/components/overlays/notification/CalloutNotification.js +6 -0
  123. package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  124. package/dist/temp/src/components/overlays/notification/FloatingNotification.js +81 -13
  125. package/dist/temp/src/components/overlays/notification/Notification.d.ts +18 -3
  126. package/dist/temp/src/components/overlays/notification/Notification.js +4 -0
  127. package/dist/temp/src/components/overlays/notification/host.d.ts +9 -0
  128. package/dist/temp/src/components/overlays/notification/host.js +9 -0
  129. package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  130. package/dist/temp/src/components/overlays/tooltip/Tooltip.js +25 -22
  131. package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  132. package/dist/temp/src/components/select-dropdown/SelectDropdown.js +2 -2
  133. package/dist/temp/src/contexts/FloatingContext.d.ts +6 -0
  134. package/dist/temp/src/contexts/FloatingContext.js +4 -0
  135. package/dist/temp/src/contexts/index.d.ts +1 -0
  136. package/dist/temp/src/contexts/index.js +1 -0
  137. package/dist/temp/src/hooks/index.d.ts +1 -0
  138. package/dist/temp/src/hooks/index.js +1 -0
  139. package/dist/temp/src/hooks/useFloatingPosition.d.ts +19 -0
  140. package/dist/temp/src/hooks/useFloatingPosition.js +55 -0
  141. package/dist/temp/src/hooks/usePortalState.d.ts +6 -0
  142. package/dist/temp/src/hooks/usePortalState.js +7 -0
  143. package/dist/temp/src/types/component-meta.d.ts +6 -2
  144. package/dist/temp/src/types/component-meta.js +14 -1
  145. package/dist/temp/src/utils/dropdown/maxSelection.d.ts +24 -0
  146. package/dist/temp/src/utils/dropdown/maxSelection.js +28 -0
  147. package/dist/temp/src/utils/dropdown/multiSelect.d.ts +42 -2
  148. package/dist/temp/src/utils/dropdown/multiSelect.js +66 -13
  149. package/dist/types/assets/scripts/featuredIcon.d.ts +22 -0
  150. package/dist/types/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  151. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  152. package/dist/types/assets/scripts/notification/MessageNotification.d.ts +22 -0
  153. package/dist/types/assets/scripts/notification/Notification.d.ts +22 -0
  154. package/dist/types/assets/scripts/notification/const/classNames.d.ts +43 -0
  155. package/dist/types/assets/scripts/notification/const/icons.d.ts +25 -0
  156. package/dist/types/assets/scripts/notification/const/index.d.ts +5 -0
  157. package/dist/types/assets/scripts/notification/const/sizes.d.ts +32 -0
  158. package/dist/types/assets/scripts/notification/const/types.d.ts +19 -0
  159. package/dist/types/assets/scripts/notification/index.d.ts +8 -0
  160. package/dist/types/assets/scripts/notification/positionSync.d.ts +50 -0
  161. package/dist/types/assets/scripts/notification/utils.d.ts +8 -0
  162. package/dist/types/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  163. package/dist/types/assets/scripts/utils/sanitize.d.ts +22 -0
  164. package/dist/types/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  165. package/dist/types/src/components/data-display/table/Table.d.ts +4 -1
  166. package/dist/types/src/components/data-display/table/types.d.ts +18 -0
  167. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  168. package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  169. package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  170. package/dist/types/src/components/forms-and-input/slider/Slider.d.ts +0 -1
  171. package/dist/types/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  172. package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  173. package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  174. package/dist/types/src/components/overlays/notification/Notification.d.ts +18 -3
  175. package/dist/types/src/components/overlays/notification/host.d.ts +9 -0
  176. package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  177. package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  178. package/dist/types/src/contexts/FloatingContext.d.ts +6 -0
  179. package/dist/types/src/contexts/index.d.ts +1 -0
  180. package/dist/types/src/hooks/index.d.ts +1 -0
  181. package/dist/types/src/hooks/useFloatingPosition.d.ts +19 -0
  182. package/dist/types/src/hooks/usePortalState.d.ts +6 -0
  183. package/dist/types/src/types/component-meta.d.ts +6 -2
  184. package/dist/types/src/utils/dropdown/maxSelection.d.ts +24 -0
  185. package/dist/types/src/utils/dropdown/multiSelect.d.ts +42 -2
  186. package/dist/ui-admin/assets/styles/style.css +312 -64
  187. package/package.json +1 -1
@@ -40,4 +40,3 @@ export const Slider = ({ min = 0, max = 100, step = 1, value = 0, onChange, labe
40
40
  }, [disabled]);
41
41
  return _jsx("div", { ref: containerRef, className: `ncua-slider ${className || ''}` });
42
42
  };
43
- export default Slider;
@@ -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 };
@@ -1,56 +1,124 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AlertCircle, AlertTriangle, CheckCircle, Pin02 } from '@ncds/ui-admin-icon';
3
3
  import classNames from 'classnames';
4
- import { forwardRef, useEffect, useRef, useState } from 'react';
4
+ import { forwardRef, useEffect, useId, useRef, useState } from 'react';
5
+ import { createPortal } from 'react-dom';
5
6
  import { MEDIA_QUERY } from '../../../constant/breakpoint';
6
7
  import { useMediaQuery } from '../../../hooks/useMediaQuery';
7
8
  import { Button } from '../../action/button';
8
9
  import { ButtonCloseX } from '../../action/button/ButtonCloseX';
9
10
  import { FeaturedIcon, } from '../../image-and-icons/featured-icon/FeaturedIcon';
11
+ // 호스트 싱글톤은 vanilla/React 양쪽이 동일한 함수를 공유한다 — host.ts 가 가까운 진입점 역할을
12
+ // 하므로 React 컴포넌트는 deep relative path 로 vanilla internals 를 직접 import 하지 않는다.
13
+ import { mountFloatingNotificationHost } from './host';
14
+ /**
15
+ * 색상별 a11y role.
16
+ * - error/warning → role="alert" (implicit aria-live="assertive": 즉시 발화)
17
+ * - 그 외 → role="status" (implicit aria-live="polite": 현재 발화 끝난 후 발화)
18
+ * role 이 implicit live-region 을 가져오므로 호스트 컨테이너에는 aria-live 를 두지 않는다.
19
+ */
20
+ const ASSERTIVE_COLORS = {
21
+ error: true,
22
+ warning: true,
23
+ };
10
24
  const iconMap = {
11
25
  neutral: Pin02,
12
26
  error: AlertTriangle,
13
27
  warning: AlertCircle,
14
28
  success: CheckCircle,
15
- // info는 floating에서는 지원하지 않음
29
+ // info full-width 전용이라 floating 아이콘 매핑 없음.
30
+ };
31
+ /**
32
+ * NotificationColor → FeaturedIconColor 매핑.
33
+ * `info` 는 FeaturedIconColor 에 존재하지 않으므로 `neutral` 로 안전 fallback.
34
+ * `satisfies` 가 매핑 누락을 컴파일 타임에 강제하므로 향후 색상이 추가되면 즉시 타입 에러로 잡힌다.
35
+ */
36
+ const iconColorMap = {
37
+ neutral: 'neutral',
38
+ error: 'error',
39
+ warning: 'warning',
40
+ success: 'success',
41
+ info: 'neutral',
16
42
  };
17
- const FloatingNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, ...rest }, ref) => {
43
+ /**
44
+ * `.ncua-floating-notification-host` 싱글톤을 보장하고 반환하는 훅.
45
+ *
46
+ * 이름의 `useMount...` 는 단순 조회가 아닌 **DOM 부수효과** 를 동반함을 신호한다 —
47
+ * 내부적으로 `mountFloatingNotificationHost()` 를 호출해 document.body 에 호스트를 append 하고
48
+ * (없으면) 글로벌 scroll/resize/MutationObserver 까지 부착한다. 호스트는 페이지 lifetime
49
+ * 싱글톤이므로 언마운트 시 제거하지 않는다.
50
+ *
51
+ * `mountFloatingNotificationHost` 는 **동기 함수** 이므로 effect 안의 호출 → setHost 가 같은 tick
52
+ * 에 끝나 cancellation race 가 없다. cancel flag 는 의미 없어 두지 않는다.
53
+ */
54
+ function useMountFloatingNotificationHost(enabled) {
55
+ const [host, setHost] = useState(null);
56
+ useEffect(() => {
57
+ if (!enabled)
58
+ return;
59
+ setHost(mountFloatingNotificationHost());
60
+ }, [enabled]);
61
+ return host;
62
+ }
63
+ const FloatingNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, portal = false, ...rest }, ref) => {
18
64
  const [shouldRemove, setShouldRemove] = useState(false);
19
- const iconColor = color;
20
65
  const featuredIconProps = {
21
66
  icon: iconMap[color] || Pin02,
22
67
  size: 'sm',
23
- color: iconColor,
68
+ color: iconColorMap[color],
24
69
  theme: 'dark-circle',
25
70
  };
26
71
  const isMobile = useMediaQuery(MEDIA_QUERY.mobile, {
27
72
  onMatched: onClose,
28
73
  });
74
+ // onClose 는 매 렌더 새 함수 ref 일 수 있으므로 ref 로 latest 만 보관.
75
+ // → autoClose 타이머 effect 의 deps 에서 onClose 를 빼서 타이머 재시작 회피.
76
+ // render 본문에서 직접 할당 — useEffect 로 미루면 같은 commit 안에서 fire 되는 타이머가 stale onClose 를 볼 위험.
77
+ const onCloseRef = useRef(onClose);
78
+ onCloseRef.current = onClose;
29
79
  // autoClose 타이머 관리
30
80
  const timerRef = useRef(null);
81
+ // onClose 는 onCloseRef 로 latest 만 추적하므로 deps 에서 제외 (의도된 패턴).
31
82
  useEffect(() => {
32
- // autoClose가 0보다 크면 무조건 타이머 설정
83
+ // autoClose 가 0 보다 크면 타이머 설정.
33
84
  if (autoClose > 0) {
34
85
  timerRef.current = setTimeout(() => {
35
- if (onClose) {
36
- onClose();
37
- }
38
- // DOM에서 바로 제거
86
+ onCloseRef.current?.();
87
+ // DOM 에서 바로 제거
39
88
  setShouldRemove(true);
40
89
  }, autoClose);
41
90
  }
42
- // cleanup 함수: 컴포넌트 언마운트 시 타이머 정리
91
+ // cleanup: 컴포넌트 언마운트 또는 autoClose 변경 시 타이머 정리
43
92
  return () => {
44
93
  if (timerRef.current) {
45
94
  clearTimeout(timerRef.current);
46
95
  timerRef.current = null;
47
96
  }
48
97
  };
49
- }, [autoClose, onClose]);
98
+ }, [autoClose]);
99
+ // portal=true 일 때만 호스트가 필요하다.
100
+ const host = useMountFloatingNotificationHost(portal);
101
+ // a11y — 카드 root 의 role 과 제목/본문 ID. 색상에 따라 alert(error/warning) / status(나머지) 로 분기.
102
+ const reactId = useId();
103
+ const titleId = `${reactId}-title`;
104
+ const descId = `${reactId}-desc`;
105
+ const role = ASSERTIVE_COLORS[color] ? 'alert' : 'status';
50
106
  // DOM에서 완전히 제거
51
107
  if (shouldRemove) {
52
108
  return null;
53
109
  }
54
- return (_jsxs("div", { ref: ref, className: classNames('ncua-floating-notification', `ncua-floating-notification--${color}`, className), role: "alert", ...rest, children: [_jsx("div", { className: "ncua-floating-notification__content", children: _jsxs("div", { className: "ncua-floating-notification__container", children: [iconMap[color] && _jsx(FeaturedIcon, { ...featuredIconProps, size: isMobile ? 'md' : 'sm' }), _jsxs("div", { className: "ncua-floating-notification__text-container", children: [_jsx("div", { className: "ncua-floating-notification__title-wrapper", children: _jsx("span", { className: "ncua-floating-notification__title", children: title }) }), supportingText && _jsx("span", { className: "ncua-floating-notification__supporting-text", children: supportingText }), actions && (_jsx("div", { className: "ncua-floating-notification__actions", children: actions.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action?.onClick }, `${action.label}-${action.hierarchy}`))) }))] })] }) }), onClose && (_jsx(ButtonCloseX, { theme: "light", className: "ncua-floating-notification__close-button", size: isMobile ? 'sm' : 'xs', onClick: onClose }))] }));
110
+ const card = (
111
+ // a11y/예측가능성 — caller 가 className/id 등 일부는 덮어쓸 수 있게 {...rest} 를 먼저 펼치고,
112
+ // role/aria-labelledby/aria-describedby 같은 컴포넌트 본질 prop 은 그 뒤에 두어 우선 적용.
113
+ _jsxs("div", { ...rest, ref: ref, className: classNames('ncua-floating-notification', `ncua-floating-notification--${color}`, className), role: role, "aria-labelledby": titleId, "aria-describedby": supportingText ? descId : undefined, children: [_jsx("div", { className: "ncua-floating-notification__content", children: _jsxs("div", { className: "ncua-floating-notification__container", children: [iconMap[color] && _jsx(FeaturedIcon, { ...featuredIconProps, size: isMobile ? 'md' : 'sm' }), _jsxs("div", { className: "ncua-floating-notification__text-container", children: [_jsx("div", { className: "ncua-floating-notification__title-wrapper", children: _jsx("span", { id: titleId, className: "ncua-floating-notification__title", children: title }) }), supportingText && (_jsx("span", { id: descId, className: "ncua-floating-notification__supporting-text", children: supportingText })), actions && (_jsx("div", { className: "ncua-floating-notification__actions", children: actions.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action?.onClick }, `${action.label}-${action.hierarchy}`))) }))] })] }) }), onClose && (_jsx(ButtonCloseX, { theme: "light", className: "ncua-floating-notification__close-button", size: isMobile ? 'sm' : 'xs', onClick: onClose }))] }));
114
+ // 기본은 인라인 렌더 — 부모 JSX 트리에 카드를 그대로 둔다.
115
+ if (!portal) {
116
+ return card;
117
+ }
118
+ // portal=true 인데 SSR이거나 첫 렌더(호스트 미생성)에서는 null. useEffect 후 host 설정되면 재렌더되어 portal 마운트.
119
+ if (!host) {
120
+ return null;
121
+ }
122
+ return createPortal(card, host);
55
123
  });
56
124
  export { FloatingNotification };
@@ -2,7 +2,8 @@ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  import type { ColorTone } from '../../../../constant/color';
3
3
  import type { SlotIconComponent } from '../../../types/side-slot';
4
4
  import type { ButtonTheme } from '../../action/button';
5
- type NotificationType = 'floating' | 'full-width' | 'message';
5
+ import { type CalloutNotificationProps } from './CalloutNotification';
6
+ type NotificationType = 'floating' | 'full-width' | 'message' | 'callout';
6
7
  type NotificationColor = Extract<ColorTone, 'neutral' | 'error' | 'warning' | 'success' | 'info'>;
7
8
  type NotificationSize = 'desktop' | 'mobile';
8
9
  interface NotificationAction {
@@ -26,9 +27,9 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
26
27
  */
27
28
  type?: NotificationType;
28
29
  /**
29
- * 알림 제목 텍스트
30
+ * 알림 제목 텍스트 (callout 유형에서는 안내 텍스트로 사용)
30
31
  */
31
- title: ReactNode;
32
+ title?: ReactNode;
32
33
  /**
33
34
  * 알림 본문 텍스트 (선택사항)
34
35
  */
@@ -66,7 +67,21 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
66
67
  * message, full-width 타입에서 사용 가능
67
68
  */
68
69
  onHidePermanently?: () => void;
70
+ /**
71
+ * Portal 마운트 여부. `floating` 타입에서만 동작한다.
72
+ *
73
+ * 기본값 `false`에서는 부모 JSX 트리 안에 카드로 그대로 렌더된다.
74
+ * `true`로 설정하면 자동으로 document.body의 `.ncua-floating-notification-host`
75
+ * 호스트에 createPortal로 마운트되어 우측 상단에 노출되고 LIFO로 스택된다
76
+ * — 최신 토스트가 상단에 노출되고 이전 토스트들이 아래로 12px씩 겹쳐 쌓인다
77
+ * (vanilla `Notification.show()`와 동일, `--ncua-floating-notification-stack-overlap`
78
+ * 변수로 조정 가능).
79
+ *
80
+ * @default false
81
+ */
82
+ portal?: boolean;
69
83
  }
70
84
  declare const Notification: import("react").ForwardRefExoticComponent<NotificationProps & import("react").RefAttributes<HTMLDivElement>>;
71
85
  export type { NotificationType, NotificationColor, NotificationSize, NotificationAction, NotificationProps };
86
+ export type { CalloutNotificationProps };
72
87
  export { Notification };
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
+ import { CalloutNotification } from './CalloutNotification';
3
4
  import { FloatingNotification } from './FloatingNotification';
4
5
  import { FullWidthNotification } from './FullWidthNotification';
5
6
  import { MessageNotification } from './MessageNotification';
@@ -13,6 +14,9 @@ const Notification = forwardRef(({ type = 'floating', color = 'neutral', ...rest
13
14
  if (type === 'message') {
14
15
  return _jsx(MessageNotification, { color: color, ...rest, ref: ref });
15
16
  }
17
+ if (type === 'callout') {
18
+ return _jsx(CalloutNotification, { color: color, ...rest });
19
+ }
16
20
  return null;
17
21
  });
18
22
  Notification.displayName = 'Notification';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Floating Notification 호스트 관리 — React 측 진입점.
3
+ *
4
+ * 실제 구현은 vanilla/React 가 공유하는 `assets/scripts/notification/positionSync.ts` 에 있고,
5
+ * 외부 노출은 그 폴더의 public barrel(`assets/scripts/notification/index.ts`)이 담당한다.
6
+ * 이 모듈은 React 컴포넌트가 vanilla 내부 파일을 직접 참조하지 않도록 barrel 을 경유하는
7
+ * 한 단계 re-export 만 제공한다 — positionSync.ts 의 위치/이름이 바뀌어도 host.ts 만 영향.
8
+ */
9
+ export { mountFloatingNotificationHost } from '../../../../assets/scripts/notification';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Floating Notification 호스트 관리 — React 측 진입점.
3
+ *
4
+ * 실제 구현은 vanilla/React 가 공유하는 `assets/scripts/notification/positionSync.ts` 에 있고,
5
+ * 외부 노출은 그 폴더의 public barrel(`assets/scripts/notification/index.ts`)이 담당한다.
6
+ * 이 모듈은 React 컴포넌트가 vanilla 내부 파일을 직접 참조하지 않도록 barrel 을 경유하는
7
+ * 한 단계 re-export 만 제공한다 — positionSync.ts 의 위치/이름이 바뀌어도 host.ts 만 영향.
8
+ */
9
+ export { mountFloatingNotificationHost } from '../../../../assets/scripts/notification';
@@ -12,6 +12,10 @@ interface TooltipProps {
12
12
  iconColor?: string;
13
13
  iconStyle?: 'help-circle' | 'alert-circle';
14
14
  zIndex?: number;
15
+ /** 외부에서 직접 패널 표시 여부를 제어 */
16
+ forceVisible?: boolean;
17
+ /** true이면 Portal 없이 앵커 내부에 패널을 인라인 렌더링. CSS 기반 위치 지정이 필요한 경우 사용 */
18
+ disablePortal?: boolean;
15
19
  }
16
- export declare const Tooltip: ({ tooltipType, iconType, position, size, title, content, hideArrow, type, iconColor, iconStyle, className, zIndex, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
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;
17
21
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AlertCircle, AlertCircleFill, HelpCircle, HelpCircleFill } from '@ncds/ui-admin-icon';
3
3
  import classNames from 'classnames';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -74,7 +74,19 @@ const computePanelCoords = (prefer, anchor, panel) => {
74
74
  top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - panel.height - VIEWPORT_MARGIN));
75
75
  return { top: Math.round(top), left: Math.round(left), calculatedPosition };
76
76
  };
77
- 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, }) => {
77
+ const buildPanelStyle = (disablePortal, coords, visible, zIndex) => {
78
+ const opacity = visible ? 1 : 0;
79
+ if (disablePortal)
80
+ return { opacity };
81
+ return { top: `${coords.top}px`, left: `${coords.left}px`, opacity, ...(zIndex && { zIndex }) };
82
+ };
83
+ const renderIcon = (iconStyle, iconType, iconSize, iconColor) => {
84
+ if (iconStyle === 'help-circle') {
85
+ return iconType === 'stroke' ? (_jsx(HelpCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(HelpCircleFill, { width: iconSize, height: iconSize, color: iconColor }));
86
+ }
87
+ return iconType === 'stroke' ? (_jsx(AlertCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(AlertCircleFill, { width: iconSize, height: iconSize, color: iconColor }));
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, }) => {
78
90
  const iconSize = size === 'sm' ? ICON_SIZE_SM : ICON_SIZE_DEFAULT;
79
91
  const anchorRef = useRef(null);
80
92
  const panelRef = useRef(null);
@@ -84,6 +96,7 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
84
96
  const [calculatedPosition, setCalculatedPosition] = useState(position === 'auto' ? 'bottom' : position);
85
97
  const [isVisible, setIsVisible] = useState(false);
86
98
  const [isManuallyClose, setIsManuallyClose] = useState(false);
99
+ const effectiveVisible = forceVisible ?? isVisible;
87
100
  useEffect(() => {
88
101
  setMounted(true);
89
102
  return () => {
@@ -94,16 +107,16 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
94
107
  };
95
108
  }, []);
96
109
  const updatePosition = useCallback(() => {
97
- if (!anchorRef.current || !panelRef.current)
110
+ if (disablePortal || !anchorRef.current || !panelRef.current)
98
111
  return;
99
112
  const anchor = anchorRef.current.getBoundingClientRect();
100
113
  const panel = panelRef.current.getBoundingClientRect();
101
114
  const next = computePanelCoords(position, anchor, panel);
102
115
  setCoords({ top: next.top, left: next.left });
103
116
  setCalculatedPosition(next.calculatedPosition);
104
- }, [position]);
117
+ }, [position, disablePortal]);
105
118
  useEffect(() => {
106
- if (!isVisible)
119
+ if (!effectiveVisible || disablePortal)
107
120
  return;
108
121
  updatePosition();
109
122
  window.addEventListener('resize', updatePosition, { passive: true });
@@ -112,14 +125,11 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
112
125
  window.removeEventListener('resize', updatePosition);
113
126
  window.removeEventListener('scroll', updatePosition, { capture: true });
114
127
  };
115
- }, [isVisible, updatePosition]);
128
+ }, [effectiveVisible, updatePosition, disablePortal]);
116
129
  const handleMouseEnter = useCallback(() => {
117
130
  if (isManuallyClose)
118
131
  return;
119
- // opacity 전환 전에 좌표 확정 (ref 가드는 updatePosition 내부)
120
132
  updatePosition();
121
- // 웹폰트 로드·max-content 재계산 등 비동기 layout 안정화 후 한 번 더 보정
122
- // 빠른 hover in/out 시 이전 프레임 요청은 취소해 중복/unmount 후 실행 방지
123
133
  if (rafIdRef.current !== null)
124
134
  cancelAnimationFrame(rafIdRef.current);
125
135
  rafIdRef.current = requestAnimationFrame(() => {
@@ -144,19 +154,12 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
144
154
  'ncua-tooltip--stroke': iconType === 'stroke',
145
155
  'ncua-tooltip--auto': position === 'auto',
146
156
  }, className), [size, type, hideArrow, iconType, position, className]);
147
- const panelClassName = useMemo(() => classNames('ncua-tooltip-panel', 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
148
- 'ncua-tooltip__bg--visible': isVisible,
149
- 'ncua-tooltip__bg--force-hidden': isManuallyClose,
150
- }), [tooltipType, finalPosition, isVisible, isManuallyClose]);
151
- const panelStyle = {
152
- top: `${coords.top}px`,
153
- left: `${coords.left}px`,
154
- opacity: isVisible ? 1 : 0,
155
- ...(zIndex && { zIndex }),
156
- };
157
+ const panelClassName = useMemo(() => classNames({ 'ncua-tooltip-panel': !disablePortal }, 'ncua-tooltip__bg', `ncua-tooltip__bg--${tooltipType}`, `ncua-tooltip__bg--${finalPosition}`, {
158
+ 'ncua-tooltip__bg--visible': effectiveVisible,
159
+ 'ncua-tooltip__bg--force-hidden': isManuallyClose && !forceVisible,
160
+ }), [tooltipType, finalPosition, effectiveVisible, isManuallyClose, forceVisible, disablePortal]);
161
+ const panelStyle = buildPanelStyle(disablePortal, coords, effectiveVisible, zIndex);
157
162
  const portalTarget = mounted ? resolvePortalTarget() : null;
158
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" }))] }));
159
- return (_jsxs(_Fragment, { children: [_jsxs("span", { ref: anchorRef, className: tooltipClassName, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [iconStyle === 'help-circle' &&
160
- (iconType === 'stroke' ? (_jsx(HelpCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(HelpCircleFill, { width: iconSize, height: iconSize, color: iconColor }))), iconStyle === 'alert-circle' &&
161
- (iconType === 'stroke' ? (_jsx(AlertCircle, { width: iconSize, height: iconSize, color: iconColor })) : (_jsx(AlertCircleFill, { width: iconSize, height: iconSize, color: iconColor })))] }), portalTarget && createPortal(panel, portalTarget)] }));
164
+ return (_jsxs("span", { ref: anchorRef, className: tooltipClassName, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [renderIcon(iconStyle, iconType, iconSize, iconColor), disablePortal ? panel : portalTarget && createPortal(panel, portalTarget)] }));
162
165
  };
@@ -17,6 +17,12 @@ type SelectDropdownProps = ComponentPropsWithRef<'div'> & {
17
17
  multiple?: boolean;
18
18
  showFooterButtons?: boolean;
19
19
  selectAllButtonText?: string;
20
+ /**
21
+ * footer의 "전체 선택" Link 노출 여부 (default: `true`).
22
+ * footer 자체(편집/선택 완료 버튼)는 영향받지 않는다.
23
+ * 호출자가 도메인 조건(예: 최대 선택 개수 제한)에 따라 `false`로 내리면 Link만 숨겨진다.
24
+ */
25
+ showSelectAllAction?: boolean;
20
26
  onSelectAll?: () => void;
21
27
  onEdit?: () => void;
22
28
  onComplete?: () => void;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import classNames from 'classnames';
3
3
  import { forwardRef } from 'react';
4
4
  import { Button } from '../action/button';
5
- const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', options, value, focusedIndex, maxHeight, listboxId = 'selectbox-options-default', onOptionSelect, onMouseMove, isKeyboardNavigation, onOptionHover, children, multiple = false, showFooterButtons = false, selectAllButtonText = '전체 선택', onSelectAll, onEdit, onComplete, className, activeDescendantId, componentType, align = 'left', ...props }, ref) => {
5
+ const SelectDropdown = forwardRef(({ isOpen, direction = 'down', size = 'xs', options, value, focusedIndex, maxHeight, listboxId = 'selectbox-options-default', onOptionSelect, onMouseMove, isKeyboardNavigation, onOptionHover, children, multiple = false, showFooterButtons = false, selectAllButtonText = '전체 선택', showSelectAllAction = true, onSelectAll, onEdit, onComplete, className, activeDescendantId, componentType, align = 'left', ...props }, ref) => {
6
6
  if (!isOpen)
7
7
  return null;
8
8
  return (_jsxs("div", { ref: ref, className: classNames('ncua-select-dropdown', `ncua-select-dropdown--${direction}`, `ncua-select-dropdown--${size}`, {
@@ -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 && (_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 })] })] }) }))] }));
24
24
  });
25
25
  SelectDropdown.displayName = 'SelectDropdown';
26
26
  export { SelectDropdown };
@@ -0,0 +1,6 @@
1
+ export type FloatingContextValue = {
2
+ preferPortal: boolean;
3
+ portalContainer?: HTMLElement | null;
4
+ };
5
+ export declare const FloatingProvider: import("react").Provider<FloatingContextValue | null>;
6
+ export declare const useFloatingContext: () => FloatingContextValue | null;
@@ -0,0 +1,4 @@
1
+ import { createContext, useContext } from 'react';
2
+ const FloatingContext = createContext(null);
3
+ export const FloatingProvider = FloatingContext.Provider;
4
+ export const useFloatingContext = () => useContext(FloatingContext);
@@ -0,0 +1 @@
1
+ export * from './FloatingContext';
@@ -0,0 +1 @@
1
+ export * from './FloatingContext';
@@ -1,4 +1,5 @@
1
1
  export * from './dropdown';
2
2
  export * from './useCallbackRef';
3
+ export * from './useFloatingPosition';
3
4
  export * from './useMediaQuery';
4
5
  export * from './useMergeRefs';
@@ -2,5 +2,6 @@
2
2
  // 드롭다운 관련 hooks
3
3
  export * from './dropdown';
4
4
  export * from './useCallbackRef';
5
+ export * from './useFloatingPosition';
5
6
  export * from './useMediaQuery';
6
7
  export * from './useMergeRefs';
@@ -0,0 +1,19 @@
1
+ import { type CSSProperties, type RefObject } from 'react';
2
+ type FloatingDirection = 'up' | 'down';
3
+ type UseFloatingPositionParams = {
4
+ enabled: boolean;
5
+ isOpen: boolean;
6
+ triggerRef: RefObject<HTMLElement | null>;
7
+ floatingRef: RefObject<HTMLElement | null>;
8
+ direction: FloatingDirection;
9
+ offset?: number;
10
+ align?: 'left' | 'right';
11
+ /**
12
+ * true면 floating 요소의 width를 trigger 너비에 맞춘다.
13
+ * Portal 모드에서 부모가 body로 바뀌면서 min-width: 100% 같은 CSS가 viewport 너비로
14
+ * 폭주하는 것을 막는다 (예: SelectBox 옵션 패널).
15
+ */
16
+ matchTriggerWidth?: boolean;
17
+ };
18
+ export declare const useFloatingPosition: ({ enabled, isOpen, triggerRef, floatingRef, direction, offset, align, matchTriggerWidth, }: UseFloatingPositionParams) => CSSProperties | null;
19
+ export {};