@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
@@ -0,0 +1,113 @@
1
+ import { FloatingNotification } from './FloatingNotification';
2
+ import { FullWidthNotification } from './FullWidthNotification';
3
+ import { MessageNotification } from './MessageNotification';
4
+ import { mountFloatingNotificationHost } from './positionSync';
5
+ // 통합 Notification 클래스 (글로벌 Web Notifications API와의 식별자 충돌을 피하기 위해 NcuaNotification 으로 명명)
6
+ export class NcuaNotification {
7
+ constructor(options) {
8
+ const {
9
+ type = 'floating',
10
+ ...baseOptions
11
+ } = options;
12
+ this.resolvedType = type;
13
+ if (type === 'message') {
14
+ this.instance = new MessageNotification(baseOptions);
15
+ } else if (type === 'full-width') {
16
+ this.instance = new FullWidthNotification(baseOptions);
17
+ } else {
18
+ this.instance = new FloatingNotification(baseOptions);
19
+ }
20
+ }
21
+ // 모든 메서드를 instance에 위임
22
+ getElement() {
23
+ return this.instance.getElement();
24
+ }
25
+ appendTo(parent) {
26
+ return this.instance.appendTo(parent);
27
+ }
28
+ remove() {
29
+ return this.instance.remove();
30
+ }
31
+ destroy() {
32
+ return this.instance.destroy();
33
+ }
34
+ show(parent) {
35
+ // 사용자가 부모를 명시한 경우 기존 동작 유지 (legacy 호환).
36
+ if (parent) {
37
+ this.instance.appendTo(parent);
38
+ return;
39
+ }
40
+ // §5.1 — floating 은 자동으로 우측 상단 호스트에 append.
41
+ // 시각적으로는 최신 토스트가 상단에 노출되고, 이전 토스트들은 그 아래로 12px 씩 겹쳐 쌓인다
42
+ // (LIFO, §5.4 "최신 상단" 준수. godomall5 등 기존 admin wrapper 와 동일 정책.
43
+ // 겹침 너비는 --ncua-floating-notification-stack-overlap CSS 변수로 오버라이드 가능).
44
+ //
45
+ // SSR 가드는 mountFloatingNotificationHost 내부의 isBrowserEnv() 로 위임 — null 이면
46
+ // 토스트가 표시될 환경이 아니라는 의미이므로 silently return 한다 (document.body 에 append
47
+ // 시도하는 fallthrough 는 SSR 에서 ReferenceError 를 일으키므로 금지).
48
+ if (this.resolvedType === 'floating') {
49
+ const host = mountFloatingNotificationHost();
50
+ if (host) {
51
+ this.instance.appendTo(host);
52
+ }
53
+ return;
54
+ }
55
+ // full-width / message는 기존대로 body에 append.
56
+ this.instance.appendTo(document.body);
57
+ }
58
+ // Static factory methods
59
+ static create(options) {
60
+ return new NcuaNotification(options);
61
+ }
62
+ // Convenience method for creating notifications with specific color
63
+ static createWithColor(color, title, supportingText, options) {
64
+ const autoCloseMap = {
65
+ success: 3000,
66
+ error: 5000,
67
+ warning: 3000,
68
+ info: 3000,
69
+ neutral: 0
70
+ };
71
+ return new NcuaNotification({
72
+ title,
73
+ supportingText,
74
+ color,
75
+ type: 'floating',
76
+ autoClose: autoCloseMap[color],
77
+ ...options
78
+ });
79
+ }
80
+ // 클래스를 생성하지 않고도 기본 floating 알림 생성을 하기 위한 함수들
81
+ static success(title, supportingText, options) {
82
+ const notification = NcuaNotification.createWithColor('success', title, supportingText, options);
83
+ notification.show();
84
+ return notification;
85
+ }
86
+ static error(title, supportingText, options) {
87
+ const notification = NcuaNotification.createWithColor('error', title, supportingText, options);
88
+ notification.show();
89
+ return notification;
90
+ }
91
+ static warning(title, supportingText, options) {
92
+ const notification = NcuaNotification.createWithColor('warning', title, supportingText, options);
93
+ notification.show();
94
+ return notification;
95
+ }
96
+ static info(title, supportingText, options) {
97
+ const notification = NcuaNotification.createWithColor('info', title, supportingText, options);
98
+ notification.show();
99
+ return notification;
100
+ }
101
+ static neutral(title, supportingText, options) {
102
+ const notification = NcuaNotification.createWithColor('neutral', title, supportingText, options);
103
+ notification.show();
104
+ return notification;
105
+ }
106
+ // showFullWidth method for backward compatibility
107
+ static showFullWidth(options) {
108
+ return new NcuaNotification({
109
+ ...options,
110
+ type: 'full-width'
111
+ });
112
+ }
113
+ }
@@ -0,0 +1,44 @@
1
+ // CSS class names
2
+ export const CLASS_NAMES = {
3
+ FULL_WIDTH: {
4
+ BASE: 'ncua-full-width-notification',
5
+ CONTAINER: 'ncua-full-width-notification__container',
6
+ CONTENT: 'ncua-full-width-notification__content',
7
+ CONTENT_WRAPPER: 'ncua-full-width-notification__content-wrapper',
8
+ ICON: 'ncua-full-width-notification__icon',
9
+ TEXT_CONTAINER: 'ncua-full-width-notification__text-container',
10
+ TITLE: 'ncua-full-width-notification__title',
11
+ SUPPORTING_TEXT: 'ncua-full-width-notification__supporting-text',
12
+ ACTIONS_CONTAINER: 'ncua-full-width-notification__actions-container',
13
+ ACTIONS: 'ncua-full-width-notification__actions',
14
+ CLOSE_BUTTON: 'ncua-full-width-notification__close-button'
15
+ },
16
+ FLOATING: {
17
+ BASE: 'ncua-floating-notification',
18
+ CONTAINER: 'ncua-floating-notification__container',
19
+ CONTENT: 'ncua-floating-notification__content',
20
+ TEXT_CONTAINER: 'ncua-floating-notification__text-container',
21
+ TITLE_WRAPPER: 'ncua-floating-notification__title-wrapper',
22
+ TITLE: 'ncua-floating-notification__title',
23
+ SUPPORTING_TEXT: 'ncua-floating-notification__supporting-text',
24
+ ACTIONS: 'ncua-floating-notification__actions',
25
+ CLOSE_BUTTON: 'ncua-floating-notification__close-button'
26
+ },
27
+ MESSAGE: {
28
+ BASE: 'ncua-message-notification',
29
+ CONTAINER: 'ncua-message-notification__container',
30
+ CONTENT: 'ncua-message-notification__content',
31
+ CONTENT_WRAPPER: 'ncua-message-notification__content-wrapper',
32
+ ICON: 'ncua-message-notification__icon',
33
+ TEXT_CONTAINER: 'ncua-message-notification__text-container',
34
+ TITLE: 'ncua-message-notification__title',
35
+ SUPPORTING_TEXT: 'ncua-message-notification__supporting-text',
36
+ ACTIONS_CONTAINER: 'ncua-message-notification__actions-container',
37
+ ACTIONS: 'ncua-message-notification__actions',
38
+ HIDE_LINK: 'ncua-message-notification__hide-link',
39
+ CLOSE_BUTTON: 'ncua-message-notification__close-button'
40
+ },
41
+ COMMON: {
42
+ ACTION_BUTTON: 'ncua-notification__action-button'
43
+ }
44
+ };
@@ -0,0 +1,25 @@
1
+ // SVG 아이콘들 (크기는 동적으로 설정)
2
+ export const SVG_ICONS = {
3
+ 'pin-02': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.377 15.616 2.72 21.273m8.974-14.631-1.56 1.56a2 2 0 0 1-.264.242 1 1 0 0 1-.207.111c-.082.032-.17.05-.347.085l-3.665.733c-.952.19-1.428.286-1.65.537a1 1 0 0 0-.243.8c.046.333.39.677 1.076 1.363l7.086 7.086c.686.687 1.03 1.03 1.362 1.076a1 1 0 0 0 .801-.242c.251-.223.346-.7.537-1.651l.733-3.665c.035-.176.053-.265.085-.347a1 1 0 0 1 .11-.207c.051-.072.115-.136.242-.263l1.561-1.561c.082-.082.122-.122.167-.158q.06-.047.126-.085c.05-.029.103-.051.208-.097l2.495-1.069c.727-.312 1.091-.467 1.256-.72a1 1 0 0 0 .144-.747c-.06-.295-.34-.575-.9-1.135l-5.142-5.143c-.56-.56-.84-.84-1.135-.9a1 1 0 0 0-.748.145c-.252.165-.407.529-.72 1.256l-1.068 2.495a2 2 0 0 1-.097.208 1 1 0 0 1-.085.126 2 2 0 0 1-.158.167"></path></svg>`,
4
+ 'alert-triangle': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M10.615 3.892 2.39 18.098c-.456.788-.684 1.182-.65 1.506a1 1 0 0 0 .406.705c.263.191.718.191 1.629.191h16.45c.91 0 1.365 0 1.628-.191a1 1 0 0 0 .407-.705c.034-.324-.195-.718-.65-1.506L13.383 3.892c-.454-.785-.681-1.178-.978-1.31a1 1 0 0 0-.812 0c-.297.132-.524.525-.979 1.31"></path></svg>`,
5
+ 'alert-circle': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`,
6
+ 'check-circle': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m7.5 12 3 3 6-6m5.5 3c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`,
7
+ 'info-circle': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none" color="#5720B7" class="ncua-full-width-notification__icon"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`,
8
+ 'message-chat-square': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none" color="#0C111D" class="ncua-full-width-notification__icon"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m10 15-3.075 3.114c-.43.434-.644.651-.828.666a.5.5 0 0 1-.421-.172c-.12-.14-.12-.446-.12-1.056v-1.56c0-.548-.449-.944-.99-1.024v0a3 3 0 0 1-2.534-2.533C2 12.219 2 11.96 2 11.445V6.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.311-1.311C4.28 2 5.12 2 6.8 2h7.4c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.311 1.311C19 4.28 19 5.12 19 6.8V11m0 11-2.176-1.513c-.306-.213-.46-.32-.626-.395a2 2 0 0 0-.462-.145c-.18-.033-.367-.033-.74-.033H13.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C10 18.394 10 17.834 10 16.714V14.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C11.52 11 12.08 11 13.2 11h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C22 12.52 22 13.08 22 14.2v2.714c0 .932 0 1.398-.152 1.766a2 2 0 0 1-1.083 1.082c-.367.152-.833.152-1.765.152z"></path></svg>`,
9
+ 'x-close': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6 6 18M6 6l12 12"></path></svg>`
10
+ };
11
+ export const FLOATING_ICON_MAP = {
12
+ neutral: SVG_ICONS['pin-02'],
13
+ error: SVG_ICONS['alert-triangle'],
14
+ warning: SVG_ICONS['alert-circle'],
15
+ success: SVG_ICONS['check-circle']
16
+ // info는 floating에서는 지원하지 않음
17
+ };
18
+ export const FULL_WIDTH_ICON_MAP = {
19
+ neutral: SVG_ICONS['message-chat-square'],
20
+ error: SVG_ICONS['alert-triangle'],
21
+ warning: SVG_ICONS['alert-triangle'],
22
+ success: SVG_ICONS['check-circle'],
23
+ info: SVG_ICONS['info-circle']
24
+ };
25
+ export const ICON_MAP = FULL_WIDTH_ICON_MAP;
@@ -0,0 +1,4 @@
1
+ export { SVG_ICONS, FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP } from './icons';
2
+ export { CLASS_NAMES } from './classNames';
3
+ export { FEATURED_ICON_SIZES, ICON_PIXEL_SIZES, CLOSE_BUTTON_SIZES, CLOSE_BUTTON_SVG_SIZES, FULL_WIDTH_SIZES, MESSAGE_SIZES, getSizes } from './sizes';
4
+ export { MESSAGE_CLOSE_ICON_COLORS } from './types';
@@ -0,0 +1,40 @@
1
+ // 알림 컴포넌트 사이즈 관련 상수들
2
+ // FeaturedIcon 사이즈
3
+ export const FEATURED_ICON_SIZES = {
4
+ MOBILE: 'md',
5
+ DESKTOP: 'sm'
6
+ };
7
+ // SVG 아이콘 픽셀 사이즈
8
+ export const ICON_PIXEL_SIZES = {
9
+ MOBILE: '20',
10
+ DESKTOP: '16',
11
+ FULL_WIDTH: '16' // Full-width는 항상 고정
12
+ };
13
+ // 닫기 버튼 사이즈
14
+ export const CLOSE_BUTTON_SIZES = {
15
+ MOBILE: 'sm',
16
+ DESKTOP: 'xs'
17
+ };
18
+ // 닫기 버튼 SVG 픽셀 사이즈
19
+ export const CLOSE_BUTTON_SVG_SIZES = {
20
+ xs: 16,
21
+ sm: 20
22
+ };
23
+ // Full-width 알림 고정 사이즈
24
+ export const FULL_WIDTH_SIZES = {
25
+ ICON: '16',
26
+ CLOSE_BUTTON: '20'
27
+ };
28
+ // Message 알림 고정 사이즈
29
+ export const MESSAGE_SIZES = {
30
+ FEATURED_ICON: 'lg',
31
+ ICON_PIXEL: '24',
32
+ CLOSE_BUTTON: '20'
33
+ };
34
+ // 사이즈 유틸리티 함수들
35
+ export const getSizes = {
36
+ featuredIcon: isMobile => isMobile ? FEATURED_ICON_SIZES.MOBILE : FEATURED_ICON_SIZES.DESKTOP,
37
+ iconPixel: isMobile => isMobile ? ICON_PIXEL_SIZES.MOBILE : ICON_PIXEL_SIZES.DESKTOP,
38
+ closeButton: isMobile => isMobile ? CLOSE_BUTTON_SIZES.MOBILE : CLOSE_BUTTON_SIZES.DESKTOP,
39
+ closeButtonSvg: size => CLOSE_BUTTON_SVG_SIZES[size]
40
+ };
@@ -0,0 +1,8 @@
1
+ // Message Notification 닫기 버튼 아이콘 색상 맵
2
+ export const MESSAGE_CLOSE_ICON_COLORS = {
3
+ neutral: '#6B7280',
4
+ error: '#EF4444',
5
+ warning: '#F97316',
6
+ success: '#16A34A',
7
+ info: '#6B7280' // info는 message 타입에서 지원하지 않지만 fallback용
8
+ };
@@ -0,0 +1,10 @@
1
+ export { CLASS_NAMES, FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP, SVG_ICONS } from './const';
2
+ export { FloatingNotification } from './FloatingNotification';
3
+ export { FullWidthNotification } from './FullWidthNotification';
4
+ export { MessageNotification } from './MessageNotification';
5
+ export { NcuaNotification as Notification } from './Notification';
6
+ // DES-SPEC-027 §5.1 — Floating notification 호스트 + 위치 동기화 (vanilla/React 공유 진입점).
7
+ // mountFloatingNotificationHost 가 자동으로 startPositionSync 를 호출하므로 일반 사용자는
8
+ // start/stop/syncNow 를 직접 부를 일이 없다 — HMR/SSR/테스트 정리용으로만 노출.
9
+ export { mountFloatingNotificationHost, startPositionSync, stopPositionSync, syncNow } from './positionSync';
10
+ export * from './utils';
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Floating Notification 호스트 싱글톤 + 위치 동기화
3
+ *
4
+ * 본 모듈은 다음 두 가지 책임을 가진다:
5
+ * 1. `.ncua-floating-notification-host` 싱글톤을 document.body 에 생성·재사용 (mountFloatingNotificationHost).
6
+ * 2. `.ncua-page-title` 의 rect.bottom 을 추적해 `--ncua-page-title-bottom` CSS 변수로 갱신.
7
+ *
8
+ * 두 책임을 한 파일에 두는 이유: 호스트가 처음 생성될 때 positionSync 가 함께 시작되며,
9
+ * React 측 훅과 vanilla 측 `NcuaNotification.show()` 가 동일 함수를 공유해 호스트 생성 로직이
10
+ * 한 곳에서만 유지되도록 한다.
11
+ *
12
+ * NCDS DES-SPEC-027 §5.1
13
+ * · Toast top 좌표 = PageTitle.bottom + 16px (viewport 기준).
14
+ * · PageTitle 이 sticky 로 Default/Fixed 변형 사이 높이가 변동(120/56px)하므로 단순 height
15
+ * 가 아닌 getBoundingClientRect().bottom 을 기준으로 동적 계산해야 한다.
16
+ *
17
+ * 동작 특성:
18
+ * - 다중 호출 idempotent. ensure/start 가 여러 번 호출되어도 호스트와 리스너는 한 벌만.
19
+ * - scroll/resize 는 rAF 로 throttle.
20
+ * - PageTitle 추가/제거를 MutationObserver 로 감지해 재바인딩 — body subtree 변경마다
21
+ * 호출되므로 콜백 자체도 rAF 로 한 프레임당 한 번만 querySelector 가 돌도록 보호.
22
+ * - SSR 가드: window/document 미정의 환경에서 ensure/start 는 no-op.
23
+ *
24
+ * 내부 상태는 모듈 최상단에 흩어진 `let` 대신 한 객체로 묶어 테스트/HMR 에서 일괄 초기화·검사하기
25
+ * 쉽도록 한다. 외부에는 함수만 노출되므로 캡슐화는 유지된다.
26
+ */
27
+ const CSS_VAR = '--ncua-page-title-bottom';
28
+ const PAGE_TITLE_SELECTOR = '.ncua-page-title';
29
+ const HOST_CLASS_NAME = 'ncua-floating-notification-host';
30
+ // add/remove EventListener 의 옵션 객체를 동일 참조로 사용해 capture/passive 미스매치를 차단.
31
+ const SCROLL_OPTIONS = {
32
+ passive: true,
33
+ capture: true
34
+ };
35
+ const RESIZE_OPTIONS = {
36
+ passive: true
37
+ };
38
+ const state = {
39
+ started: false,
40
+ rafId: null,
41
+ rebindRafId: null,
42
+ pageTitle: null,
43
+ resizeObserver: null,
44
+ mutationObserver: null
45
+ };
46
+ /** window/document 가 사용 가능한 브라우저 환경인지 — SSR/Node 가드. */
47
+ function isBrowserEnv() {
48
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
49
+ }
50
+ function readBottomPx() {
51
+ if (!state.pageTitle) state.pageTitle = document.querySelector(PAGE_TITLE_SELECTOR);
52
+ if (!state.pageTitle) return 0;
53
+ const rect = state.pageTitle.getBoundingClientRect();
54
+ // viewport 기준 음수는 0으로 클램프 (PageTitle이 스크롤되어 시야에서 벗어난 경우)
55
+ return Math.max(0, Math.round(rect.bottom));
56
+ }
57
+ function flush() {
58
+ document.documentElement.style.setProperty(CSS_VAR, `${readBottomPx()}px`);
59
+ state.rafId = null;
60
+ }
61
+ function schedule() {
62
+ if (state.rafId !== null) return;
63
+ state.rafId = window.requestAnimationFrame(flush);
64
+ }
65
+ /** 현재 cached pageTitle 이 여전히 DOM 에 연결되어 있는지 — body subtree 변경 시 빠른 early-return. */
66
+ function pageTitleStillInDom() {
67
+ return state.pageTitle?.isConnected ?? false;
68
+ }
69
+ function rebindPageTitle() {
70
+ state.rebindRafId = null;
71
+ if (pageTitleStillInDom()) return;
72
+ const next = document.querySelector(PAGE_TITLE_SELECTOR);
73
+ if (next === state.pageTitle) return;
74
+ state.pageTitle = next;
75
+ if (state.resizeObserver) {
76
+ state.resizeObserver.disconnect();
77
+ // box: 'content-box' 일관성 — startPositionSync 의 첫 observe 와 동일 옵션.
78
+ if (state.pageTitle) state.resizeObserver.observe(state.pageTitle, {
79
+ box: 'content-box'
80
+ });
81
+ }
82
+ schedule();
83
+ }
84
+ /**
85
+ * MutationObserver 콜백 — body subtree 의 모든 변경마다 즉시 querySelector 를 돌리면 비싸므로
86
+ * 프레임당 한 번으로 throttle. 캐시된 pageTitle 이 아직 DOM 에 있으면 rebindPageTitle 안에서
87
+ * 추가 비용 없이 early-return 한다.
88
+ */
89
+ function scheduleRebind() {
90
+ if (state.rebindRafId !== null) return;
91
+ state.rebindRafId = window.requestAnimationFrame(rebindPageTitle);
92
+ }
93
+ /**
94
+ * `.ncua-floating-notification-host` 싱글톤을 보장한다.
95
+ * **side-effect 함수** — 단순 조회가 아니라 다음을 모두 수행:
96
+ * · document.body 에 `<div class="ncua-floating-notification-host">` 를 (필요 시) append
97
+ * · 첫 호출 시 startPositionSync() 로 window scroll/resize/MutationObserver/ResizeObserver 부착
98
+ * · 한 번 만든 호스트는 페이지 lifetime 동안 유지 — 컴포넌트 언마운트 시에도 제거하지 않는다
99
+ * (다음 토스트의 mount 비용을 줄이기 위해 의도된 설계)
100
+ *
101
+ * React 측 hook 과 vanilla 측 `NcuaNotification.show()` 가 모두 이 함수를 사용해 호스트 생성
102
+ * 진입점이 한 곳만 존재한다.
103
+ *
104
+ * @returns 생성되었거나 이미 존재하는 호스트 엘리먼트. SSR 환경에서는 null.
105
+ */
106
+ export function mountFloatingNotificationHost() {
107
+ if (!isBrowserEnv()) return null;
108
+ let host = document.querySelector(`.${HOST_CLASS_NAME}`);
109
+ if (!host) {
110
+ host = document.createElement('div');
111
+ host.className = HOST_CLASS_NAME;
112
+ // 호스트는 layout container 일 뿐 — live region 책임은 각 토스트 카드의 role 에 위임.
113
+ document.body.appendChild(host);
114
+ startPositionSync();
115
+ }
116
+ return host;
117
+ }
118
+ /**
119
+ * PageTitle ↔ Floating Host 위치 동기화 시작 (idempotent).
120
+ * `mountFloatingNotificationHost()` 가 자동 호출하므로 일반 사용자는 직접 부를 필요 없음.
121
+ * SSR/테스트에서 수동 제어가 필요할 때만 export 됨.
122
+ */
123
+ export function startPositionSync() {
124
+ if (state.started || !isBrowserEnv()) return;
125
+ state.started = true;
126
+ state.pageTitle = document.querySelector(PAGE_TITLE_SELECTOR);
127
+ window.addEventListener('scroll', schedule, SCROLL_OPTIONS);
128
+ window.addEventListener('resize', schedule, RESIZE_OPTIONS);
129
+ if (typeof ResizeObserver !== 'undefined') {
130
+ state.resizeObserver = new ResizeObserver(schedule);
131
+ // box: 'content-box' 명시 — border/padding 변경으로 인한 콜백 재호출 회피 (DOM mutation 무한 루프 방지).
132
+ if (state.pageTitle) state.resizeObserver.observe(state.pageTitle, {
133
+ box: 'content-box'
134
+ });
135
+ }
136
+ if (typeof MutationObserver !== 'undefined') {
137
+ state.mutationObserver = new MutationObserver(scheduleRebind);
138
+ state.mutationObserver.observe(document.body, {
139
+ childList: true,
140
+ subtree: true
141
+ });
142
+ }
143
+ // 첫 동기화
144
+ flush();
145
+ }
146
+ /** 동기화 중단 + 리소스 해제. 테스트 종료/HMR 정리 등에 사용. */
147
+ export function stopPositionSync() {
148
+ if (!state.started) return;
149
+ state.started = false;
150
+ window.removeEventListener('scroll', schedule, SCROLL_OPTIONS);
151
+ window.removeEventListener('resize', schedule, RESIZE_OPTIONS);
152
+ state.resizeObserver?.disconnect();
153
+ state.resizeObserver = null;
154
+ state.mutationObserver?.disconnect();
155
+ state.mutationObserver = null;
156
+ if (state.rafId !== null) {
157
+ window.cancelAnimationFrame(state.rafId);
158
+ state.rafId = null;
159
+ }
160
+ if (state.rebindRafId !== null) {
161
+ window.cancelAnimationFrame(state.rebindRafId);
162
+ state.rebindRafId = null;
163
+ }
164
+ state.pageTitle = null;
165
+ // 정리 시 CSS 변수도 초기 상태로 되돌림
166
+ document.documentElement.style.removeProperty(CSS_VAR);
167
+ }
168
+ /** 외부에서 PageTitle.bottom 값을 강제로 다시 측정해 반영하고 싶을 때. */
169
+ export function syncNow() {
170
+ flush();
171
+ }
@@ -0,0 +1,109 @@
1
+ import { BREAKPOINT } from '../../../src/constant/breakpoint';
2
+ import { CLASS_NAMES } from './const';
3
+ // 공통 유틸리티 함수들
4
+ export function createWrapperElement(baseClass, color, className) {
5
+ const wrapper = document.createElement('div');
6
+ wrapper.className = buildClassName(baseClass, color, className);
7
+ wrapper.setAttribute('role', 'alert');
8
+ return wrapper;
9
+ }
10
+ export function buildClassName(baseClass, color, className) {
11
+ const classes = [baseClass, `${baseClass}--${color}`];
12
+ if (className) {
13
+ classes.push(className);
14
+ }
15
+ return classes.join(' ');
16
+ }
17
+ /**
18
+ * http/https 프로토콜만 허용한다.
19
+ * javascript:, data:, file: 등 위험 프로토콜로 인한 XSS / 외부 페이로드 로딩을 차단.
20
+ */
21
+ function isSafeUrl(url) {
22
+ try {
23
+ const parsed = new URL(url, window.location.origin);
24
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * HTML 속성 값에 안전하게 삽입하기 위한 최소 이스케이프.
31
+ * 본문(body)이 아닌 속성 컨텍스트에서의 따옴표 탈출(`" onclick="..."`)을 방지.
32
+ */
33
+ function escapeHtmlAttr(str) {
34
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
35
+ }
36
+ export function renderSupportingText(supportingText, className, supportTextLink) {
37
+ if (!supportingText) return '';
38
+ // 본문(supportingText) 자체는 이 함수의 호출 결과가 wrapper.innerHTML에
39
+ // 들어가기 전에 DOMPurify로 sanitize되므로 별도 이스케이프하지 않는다.
40
+ // 이렇게 해야 <br>, <strong> 등 서식용 태그가 그대로 동작한다.
41
+ if (supportTextLink && isSafeUrl(supportTextLink)) {
42
+ const safeLink = escapeHtmlAttr(supportTextLink);
43
+ return `<a href="${safeLink}" class="ncua-full-width-notification__link" rel="noopener noreferrer" target="_blank"><span class="${className}">${supportingText}</span></a>`;
44
+ }
45
+ // unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
46
+ return `<span class="${className}">${supportingText}</span>`;
47
+ }
48
+ export function renderActions(actions, wrapperClass) {
49
+ // 액션이 없으면 빈 문자열 반환
50
+ if (!actions || actions.length === 0) {
51
+ return '';
52
+ }
53
+ const buttonsHtml = actions.map(action => {
54
+ const buttonHtml = `
55
+ <button
56
+ class="ncua-btn ncua-btn--sm ncua-btn--${action.hierarchy || 'text'}"
57
+ data-action="${action.label}-${action.hierarchy}"
58
+ >
59
+ ${action.label}
60
+ </button>`;
61
+ return buttonHtml;
62
+ }).join('');
63
+ return `<div class="${wrapperClass}">${buttonsHtml}</div>`;
64
+ }
65
+ // 공통 이벤트 처리
66
+ export function bindNotificationEvents(element, actions, onClose, onRemove) {
67
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 닫기/액션 버튼 분기를 한 핸들러에 묶는 기존 구조를 유지. 분리 리팩토링은 별도 작업 범위.
68
+ element.addEventListener('click', event => {
69
+ const target = event.target;
70
+ // 닫기 버튼 클릭 처리
71
+ if (target.matches(`.${CLASS_NAMES.FULL_WIDTH.CLOSE_BUTTON}, .${CLASS_NAMES.FLOATING.CLOSE_BUTTON}`) || target.closest(`.${CLASS_NAMES.FULL_WIDTH.CLOSE_BUTTON}, .${CLASS_NAMES.FLOATING.CLOSE_BUTTON}`) || target.closest(`.${CLASS_NAMES.MESSAGE.CLOSE_BUTTON}`)) {
72
+ onClose?.();
73
+ onRemove?.();
74
+ return;
75
+ }
76
+ // 액션 버튼 클릭 처리
77
+ const actionButton = target.closest('.ncua-btn[data-action]');
78
+ if (actionButton && actions) {
79
+ const actionData = actionButton.getAttribute('data-action');
80
+ if (actionData) {
81
+ let matchedAction = null;
82
+ for (const action of actions) {
83
+ const expectedDataAction = `${action.label}-${action.hierarchy || 'link'}`;
84
+ if (actionData === expectedDataAction) {
85
+ matchedAction = action;
86
+ break;
87
+ }
88
+ }
89
+ if (matchedAction?.onClick) {
90
+ matchedAction.onClick();
91
+ }
92
+ }
93
+ }
94
+ });
95
+ }
96
+ // 자동 닫기 설정
97
+ export function setupAutoClose(autoClose, onClose, onRemove) {
98
+ if (autoClose > 0) {
99
+ return window.setTimeout(() => {
100
+ onClose?.();
101
+ onRemove?.();
102
+ }, autoClose);
103
+ }
104
+ return undefined;
105
+ }
106
+ // Mobile detection utility
107
+ export const isMobile = () => {
108
+ return window.innerWidth <= Number.parseInt(BREAKPOINT.mobile, 10);
109
+ };
@@ -0,0 +1,37 @@
1
+ // 공통 X버튼 로직 (React ButtonCloseX 컴포넌트와 동일한 구조)
2
+ // React ButtonCloseX와 동일한 SVG 사이즈 매핑
3
+ export const SVG_SIZE = {
4
+ xs: 16,
5
+ sm: 20,
6
+ md: 20,
7
+ lg: 24
8
+ };
9
+ // X버튼 SVG 아이콘
10
+ export const X_CLOSE_SVG = size => `<svg width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
12
+ </svg>`;
13
+ // X버튼 렌더링 유틸리티 (React ButtonCloseX와 동일한 인터페이스)
14
+ export function ButtonCloseX(size) {
15
+ let theme = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'light';
16
+ let additionalClasses = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
17
+ let ariaLabel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '닫기';
18
+ let onClick = arguments.length > 4 ? arguments[4] : undefined;
19
+ const svgSize = SVG_SIZE[size];
20
+ const buttonId = `close-btn-${Math.random().toString(36).substr(2, 9)}`;
21
+ const buttonHTML = `
22
+ <button type="button" id="${buttonId}" class="ncua-button-close-x ncua-button-close-x--${size} ncua-button-close-x--${theme} ${additionalClasses}" aria-label="${ariaLabel}">
23
+ ${X_CLOSE_SVG(svgSize.toString())}
24
+ </button>
25
+ `;
26
+ // onClick이 제공된 경우 이벤트 바인딩
27
+ if (onClick) {
28
+ // DOM에 추가된 후 이벤트 바인딩을 위해 setTimeout 사용
29
+ setTimeout(() => {
30
+ const button = document.getElementById(buttonId);
31
+ if (button) {
32
+ button.addEventListener('click', onClick);
33
+ }
34
+ }, 0);
35
+ }
36
+ return buttonHTML;
37
+ }
@@ -0,0 +1,31 @@
1
+ import DOMPurify from 'dompurify';
2
+ /**
3
+ * HTML 문자열을 DOMPurify 기본 설정으로 sanitize한다.
4
+ *
5
+ * 제거되는 항목 (기본 설정):
6
+ * - `<script>`, `<iframe>`, `<object>` 등 실행 위험 태그
7
+ * - `onclick`, `onerror` 등 inline 이벤트 핸들러 속성
8
+ * - `href="javascript:..."` 같은 javascript: URL
9
+ *
10
+ * 유지되는 항목:
11
+ * - `<svg>`, `<button>`, `<div>`, `<span>`, `<input>` 등 일반 HTML/SVG 태그
12
+ * - `class`, `style`, `role`, `aria-*`, `data-*` 등 표현용 속성
13
+ * - SVG 표현 속성 (`viewBox`, `stroke`, `fill` 등)
14
+ *
15
+ * 이벤트 바인딩은 addEventListener로 별도 처리할 것.
16
+ */
17
+ export function sanitizeHtml(dirty) {
18
+ return DOMPurify.sanitize(dirty);
19
+ }
20
+ /**
21
+ * 엘리먼트에 콘텐츠를 안전하게 설정한다.
22
+ * - string: sanitize 후 innerHTML 교체
23
+ * - HTMLElement: 기존 자식 제거 후 appendChild
24
+ */
25
+ export function setSafeInnerHTML(element, content) {
26
+ if (content instanceof HTMLElement) {
27
+ element.replaceChildren(content);
28
+ return;
29
+ }
30
+ element.innerHTML = sanitizeHtml(content);
31
+ }
@@ -73,7 +73,9 @@ const DataGridTable = /*#__PURE__*/forwardRef((_ref5, ref) => {
73
73
  fixedHeader,
74
74
  maxHeight,
75
75
  hoverable,
76
- selectable
76
+ selectable,
77
+ horizontalScroll,
78
+ minWidth
77
79
  } = _ref5;
78
80
  return _jsx("div", {
79
81
  ref: ref,
@@ -85,6 +87,8 @@ const DataGridTable = /*#__PURE__*/forwardRef((_ref5, ref) => {
85
87
  maxHeight: maxHeight,
86
88
  hoverable: hoverable,
87
89
  selectable: selectable,
90
+ horizontalScroll: horizontalScroll,
91
+ minWidth: minWidth,
88
92
  children: children
89
93
  })
90
94
  });