@ncds/ui-admin 1.8.4 → 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 (177) 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/overlays/dropdown/Dropdown.js +47 -19
  23. package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +25 -0
  24. package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +86 -13
  25. package/dist/cjs/src/components/overlays/notification/Notification.js +7 -0
  26. package/dist/cjs/src/components/overlays/notification/host.js +12 -0
  27. package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +57 -44
  28. package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +2 -1
  29. package/dist/cjs/src/contexts/FloatingContext.js +11 -0
  30. package/dist/cjs/src/contexts/index.js +16 -0
  31. package/dist/cjs/src/hooks/index.js +11 -0
  32. package/dist/cjs/src/hooks/useFloatingPosition.js +78 -0
  33. package/dist/cjs/src/hooks/usePortalState.js +17 -0
  34. package/dist/cjs/src/utils/dropdown/maxSelection.js +35 -0
  35. package/dist/cjs/src/utils/dropdown/multiSelect.js +72 -15
  36. package/dist/esm/assets/scripts/featuredIcon.js +80 -0
  37. package/dist/esm/assets/scripts/notification/FloatingNotification.js +171 -0
  38. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +126 -0
  39. package/dist/esm/assets/scripts/notification/MessageNotification.js +152 -0
  40. package/dist/esm/assets/scripts/notification/Notification.js +113 -0
  41. package/dist/esm/assets/scripts/notification/const/classNames.js +44 -0
  42. package/dist/esm/assets/scripts/notification/const/icons.js +25 -0
  43. package/dist/esm/assets/scripts/notification/const/index.js +4 -0
  44. package/dist/esm/assets/scripts/notification/const/sizes.js +40 -0
  45. package/dist/esm/assets/scripts/notification/const/types.js +8 -0
  46. package/dist/esm/assets/scripts/notification/index.js +10 -0
  47. package/dist/esm/assets/scripts/notification/positionSync.js +171 -0
  48. package/dist/esm/assets/scripts/notification/utils.js +109 -0
  49. package/dist/esm/assets/scripts/shared/ButtonCloseX.js +37 -0
  50. package/dist/esm/assets/scripts/utils/sanitize.js +31 -0
  51. package/dist/esm/src/components/data-display/data-grid/DataGrid.js +5 -1
  52. package/dist/esm/src/components/data-display/table/Table.js +118 -96
  53. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +179 -0
  54. package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
  55. package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
  56. package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +67 -29
  57. package/dist/esm/src/components/overlays/dropdown/Dropdown.js +47 -19
  58. package/dist/esm/src/components/overlays/notification/CalloutNotification.js +19 -0
  59. package/dist/esm/src/components/overlays/notification/FloatingNotification.js +86 -14
  60. package/dist/esm/src/components/overlays/notification/Notification.js +7 -0
  61. package/dist/esm/src/components/overlays/notification/host.js +9 -0
  62. package/dist/esm/src/components/overlays/tooltip/Tooltip.js +58 -45
  63. package/dist/esm/src/components/select-dropdown/SelectDropdown.js +2 -1
  64. package/dist/esm/src/contexts/FloatingContext.js +4 -0
  65. package/dist/esm/src/contexts/index.js +1 -0
  66. package/dist/esm/src/hooks/index.js +1 -0
  67. package/dist/esm/src/hooks/useFloatingPosition.js +71 -0
  68. package/dist/esm/src/hooks/usePortalState.js +10 -0
  69. package/dist/esm/src/utils/dropdown/maxSelection.js +27 -0
  70. package/dist/esm/src/utils/dropdown/multiSelect.js +70 -14
  71. package/dist/temp/assets/scripts/featuredIcon.d.ts +22 -0
  72. package/dist/temp/assets/scripts/featuredIcon.js +79 -0
  73. package/dist/temp/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  74. package/dist/temp/assets/scripts/notification/FloatingNotification.js +156 -0
  75. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  76. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +111 -0
  77. package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +22 -0
  78. package/dist/temp/assets/scripts/notification/MessageNotification.js +140 -0
  79. package/dist/temp/assets/scripts/notification/Notification.d.ts +22 -0
  80. package/dist/temp/assets/scripts/notification/Notification.js +112 -0
  81. package/dist/temp/assets/scripts/notification/const/classNames.d.ts +43 -0
  82. package/dist/temp/assets/scripts/notification/const/classNames.js +44 -0
  83. package/dist/temp/assets/scripts/notification/const/icons.d.ts +25 -0
  84. package/dist/temp/assets/scripts/notification/const/icons.js +25 -0
  85. package/dist/temp/assets/scripts/notification/const/index.d.ts +5 -0
  86. package/dist/temp/assets/scripts/notification/const/index.js +4 -0
  87. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +32 -0
  88. package/dist/temp/assets/scripts/notification/const/sizes.js +40 -0
  89. package/dist/temp/assets/scripts/notification/const/types.d.ts +19 -0
  90. package/dist/temp/assets/scripts/notification/const/types.js +8 -0
  91. package/dist/temp/assets/scripts/notification/index.d.ts +8 -0
  92. package/dist/temp/assets/scripts/notification/index.js +10 -0
  93. package/dist/temp/assets/scripts/notification/positionSync.d.ts +50 -0
  94. package/dist/temp/assets/scripts/notification/positionSync.js +170 -0
  95. package/dist/temp/assets/scripts/notification/utils.d.ts +8 -0
  96. package/dist/temp/assets/scripts/notification/utils.js +115 -0
  97. package/dist/temp/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  98. package/dist/temp/assets/scripts/shared/ButtonCloseX.js +33 -0
  99. package/dist/temp/assets/scripts/utils/sanitize.d.ts +22 -0
  100. package/dist/temp/assets/scripts/utils/sanitize.js +31 -0
  101. package/dist/temp/src/components/data-display/data-grid/DataGrid.js +1 -1
  102. package/dist/temp/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  103. package/dist/temp/src/components/data-display/table/Table.d.ts +4 -1
  104. package/dist/temp/src/components/data-display/table/Table.js +53 -68
  105. package/dist/temp/src/components/data-display/table/types.d.ts +18 -0
  106. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  107. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +136 -0
  108. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  109. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +7 -11
  110. package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +1 -1
  111. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  112. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +30 -3
  113. package/dist/temp/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  114. package/dist/temp/src/components/overlays/dropdown/Dropdown.js +35 -11
  115. package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  116. package/dist/temp/src/components/overlays/notification/CalloutNotification.js +6 -0
  117. package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  118. package/dist/temp/src/components/overlays/notification/FloatingNotification.js +81 -13
  119. package/dist/temp/src/components/overlays/notification/Notification.d.ts +18 -3
  120. package/dist/temp/src/components/overlays/notification/Notification.js +4 -0
  121. package/dist/temp/src/components/overlays/notification/host.d.ts +9 -0
  122. package/dist/temp/src/components/overlays/notification/host.js +9 -0
  123. package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  124. package/dist/temp/src/components/overlays/tooltip/Tooltip.js +25 -22
  125. package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  126. package/dist/temp/src/components/select-dropdown/SelectDropdown.js +2 -2
  127. package/dist/temp/src/contexts/FloatingContext.d.ts +6 -0
  128. package/dist/temp/src/contexts/FloatingContext.js +4 -0
  129. package/dist/temp/src/contexts/index.d.ts +1 -0
  130. package/dist/temp/src/contexts/index.js +1 -0
  131. package/dist/temp/src/hooks/index.d.ts +1 -0
  132. package/dist/temp/src/hooks/index.js +1 -0
  133. package/dist/temp/src/hooks/useFloatingPosition.d.ts +19 -0
  134. package/dist/temp/src/hooks/useFloatingPosition.js +55 -0
  135. package/dist/temp/src/hooks/usePortalState.d.ts +6 -0
  136. package/dist/temp/src/hooks/usePortalState.js +7 -0
  137. package/dist/temp/src/utils/dropdown/maxSelection.d.ts +24 -0
  138. package/dist/temp/src/utils/dropdown/maxSelection.js +28 -0
  139. package/dist/temp/src/utils/dropdown/multiSelect.d.ts +42 -2
  140. package/dist/temp/src/utils/dropdown/multiSelect.js +66 -13
  141. package/dist/types/assets/scripts/featuredIcon.d.ts +22 -0
  142. package/dist/types/assets/scripts/notification/FloatingNotification.d.ts +24 -0
  143. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
  144. package/dist/types/assets/scripts/notification/MessageNotification.d.ts +22 -0
  145. package/dist/types/assets/scripts/notification/Notification.d.ts +22 -0
  146. package/dist/types/assets/scripts/notification/const/classNames.d.ts +43 -0
  147. package/dist/types/assets/scripts/notification/const/icons.d.ts +25 -0
  148. package/dist/types/assets/scripts/notification/const/index.d.ts +5 -0
  149. package/dist/types/assets/scripts/notification/const/sizes.d.ts +32 -0
  150. package/dist/types/assets/scripts/notification/const/types.d.ts +19 -0
  151. package/dist/types/assets/scripts/notification/index.d.ts +8 -0
  152. package/dist/types/assets/scripts/notification/positionSync.d.ts +50 -0
  153. package/dist/types/assets/scripts/notification/utils.d.ts +8 -0
  154. package/dist/types/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
  155. package/dist/types/assets/scripts/utils/sanitize.d.ts +22 -0
  156. package/dist/types/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
  157. package/dist/types/src/components/data-display/table/Table.d.ts +4 -1
  158. package/dist/types/src/components/data-display/table/types.d.ts +18 -0
  159. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
  160. package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
  161. package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
  162. package/dist/types/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
  163. package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
  164. package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
  165. package/dist/types/src/components/overlays/notification/Notification.d.ts +18 -3
  166. package/dist/types/src/components/overlays/notification/host.d.ts +9 -0
  167. package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
  168. package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
  169. package/dist/types/src/contexts/FloatingContext.d.ts +6 -0
  170. package/dist/types/src/contexts/index.d.ts +1 -0
  171. package/dist/types/src/hooks/index.d.ts +1 -0
  172. package/dist/types/src/hooks/useFloatingPosition.d.ts +19 -0
  173. package/dist/types/src/hooks/usePortalState.d.ts +6 -0
  174. package/dist/types/src/utils/dropdown/maxSelection.d.ts +24 -0
  175. package/dist/types/src/utils/dropdown/multiSelect.d.ts +42 -2
  176. package/dist/ui-admin/assets/styles/style.css +304 -64
  177. package/package.json +1 -1
@@ -0,0 +1,170 @@
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 = { passive: true, capture: true };
32
+ const RESIZE_OPTIONS = { passive: true };
33
+ const state = {
34
+ started: false,
35
+ rafId: null,
36
+ rebindRafId: null,
37
+ pageTitle: null,
38
+ resizeObserver: null,
39
+ mutationObserver: null,
40
+ };
41
+ /** window/document 가 사용 가능한 브라우저 환경인지 — SSR/Node 가드. */
42
+ function isBrowserEnv() {
43
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
44
+ }
45
+ function readBottomPx() {
46
+ if (!state.pageTitle)
47
+ state.pageTitle = document.querySelector(PAGE_TITLE_SELECTOR);
48
+ if (!state.pageTitle)
49
+ return 0;
50
+ const rect = state.pageTitle.getBoundingClientRect();
51
+ // viewport 기준 음수는 0으로 클램프 (PageTitle이 스크롤되어 시야에서 벗어난 경우)
52
+ return Math.max(0, Math.round(rect.bottom));
53
+ }
54
+ function flush() {
55
+ document.documentElement.style.setProperty(CSS_VAR, `${readBottomPx()}px`);
56
+ state.rafId = null;
57
+ }
58
+ function schedule() {
59
+ if (state.rafId !== null)
60
+ return;
61
+ state.rafId = window.requestAnimationFrame(flush);
62
+ }
63
+ /** 현재 cached pageTitle 이 여전히 DOM 에 연결되어 있는지 — body subtree 변경 시 빠른 early-return. */
64
+ function pageTitleStillInDom() {
65
+ return state.pageTitle?.isConnected ?? false;
66
+ }
67
+ function rebindPageTitle() {
68
+ state.rebindRafId = null;
69
+ if (pageTitleStillInDom())
70
+ return;
71
+ const next = document.querySelector(PAGE_TITLE_SELECTOR);
72
+ if (next === state.pageTitle)
73
+ return;
74
+ state.pageTitle = next;
75
+ if (state.resizeObserver) {
76
+ state.resizeObserver.disconnect();
77
+ // box: 'content-box' 일관성 — startPositionSync 의 첫 observe 와 동일 옵션.
78
+ if (state.pageTitle)
79
+ state.resizeObserver.observe(state.pageTitle, { box: 'content-box' });
80
+ }
81
+ schedule();
82
+ }
83
+ /**
84
+ * MutationObserver 콜백 — body subtree 의 모든 변경마다 즉시 querySelector 를 돌리면 비싸므로
85
+ * 프레임당 한 번으로 throttle. 캐시된 pageTitle 이 아직 DOM 에 있으면 rebindPageTitle 안에서
86
+ * 추가 비용 없이 early-return 한다.
87
+ */
88
+ function scheduleRebind() {
89
+ if (state.rebindRafId !== null)
90
+ 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())
108
+ return null;
109
+ let host = document.querySelector(`.${HOST_CLASS_NAME}`);
110
+ if (!host) {
111
+ host = document.createElement('div');
112
+ host.className = HOST_CLASS_NAME;
113
+ // 호스트는 layout container 일 뿐 — live region 책임은 각 토스트 카드의 role 에 위임.
114
+ document.body.appendChild(host);
115
+ startPositionSync();
116
+ }
117
+ return host;
118
+ }
119
+ /**
120
+ * PageTitle ↔ Floating Host 위치 동기화 시작 (idempotent).
121
+ * `mountFloatingNotificationHost()` 가 자동 호출하므로 일반 사용자는 직접 부를 필요 없음.
122
+ * SSR/테스트에서 수동 제어가 필요할 때만 export 됨.
123
+ */
124
+ export function startPositionSync() {
125
+ if (state.started || !isBrowserEnv())
126
+ return;
127
+ state.started = true;
128
+ state.pageTitle = document.querySelector(PAGE_TITLE_SELECTOR);
129
+ window.addEventListener('scroll', schedule, SCROLL_OPTIONS);
130
+ window.addEventListener('resize', schedule, RESIZE_OPTIONS);
131
+ if (typeof ResizeObserver !== 'undefined') {
132
+ state.resizeObserver = new ResizeObserver(schedule);
133
+ // box: 'content-box' 명시 — border/padding 변경으로 인한 콜백 재호출 회피 (DOM mutation 무한 루프 방지).
134
+ if (state.pageTitle)
135
+ state.resizeObserver.observe(state.pageTitle, { box: 'content-box' });
136
+ }
137
+ if (typeof MutationObserver !== 'undefined') {
138
+ state.mutationObserver = new MutationObserver(scheduleRebind);
139
+ state.mutationObserver.observe(document.body, { childList: true, subtree: true });
140
+ }
141
+ // 첫 동기화
142
+ flush();
143
+ }
144
+ /** 동기화 중단 + 리소스 해제. 테스트 종료/HMR 정리 등에 사용. */
145
+ export function stopPositionSync() {
146
+ if (!state.started)
147
+ return;
148
+ state.started = false;
149
+ window.removeEventListener('scroll', schedule, SCROLL_OPTIONS);
150
+ window.removeEventListener('resize', schedule, RESIZE_OPTIONS);
151
+ state.resizeObserver?.disconnect();
152
+ state.resizeObserver = null;
153
+ state.mutationObserver?.disconnect();
154
+ state.mutationObserver = null;
155
+ if (state.rafId !== null) {
156
+ window.cancelAnimationFrame(state.rafId);
157
+ state.rafId = null;
158
+ }
159
+ if (state.rebindRafId !== null) {
160
+ window.cancelAnimationFrame(state.rebindRafId);
161
+ state.rebindRafId = null;
162
+ }
163
+ state.pageTitle = null;
164
+ // 정리 시 CSS 변수도 초기 상태로 되돌림
165
+ document.documentElement.style.removeProperty(CSS_VAR);
166
+ }
167
+ /** 외부에서 PageTitle.bottom 값을 강제로 다시 측정해 반영하고 싶을 때. */
168
+ export function syncNow() {
169
+ flush();
170
+ }
@@ -0,0 +1,8 @@
1
+ import type { NotificationAction, NotificationColor } from './const';
2
+ export declare function createWrapperElement(baseClass: string, color: NotificationColor, className?: string): HTMLElement;
3
+ export declare function buildClassName(baseClass: string, color: NotificationColor, className?: string): string;
4
+ export declare function renderSupportingText(supportingText?: string, className?: string, supportTextLink?: string): string;
5
+ export declare function renderActions(actions: NotificationAction[], wrapperClass: string): string;
6
+ export declare function bindNotificationEvents(element: HTMLElement, actions: NotificationAction[], onClose?: () => void, onRemove?: () => void): void;
7
+ export declare function setupAutoClose(autoClose: number, onClose?: () => void, onRemove?: () => void): number | undefined;
8
+ export declare const isMobile: () => boolean;
@@ -0,0 +1,115 @@
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
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * HTML 속성 값에 안전하게 삽입하기 위한 최소 이스케이프.
32
+ * 본문(body)이 아닌 속성 컨텍스트에서의 따옴표 탈출(`" onclick="..."`)을 방지.
33
+ */
34
+ function escapeHtmlAttr(str) {
35
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
36
+ }
37
+ export function renderSupportingText(supportingText, className, supportTextLink) {
38
+ if (!supportingText)
39
+ return '';
40
+ // 본문(supportingText) 자체는 이 함수의 호출 결과가 wrapper.innerHTML에
41
+ // 들어가기 전에 DOMPurify로 sanitize되므로 별도 이스케이프하지 않는다.
42
+ // 이렇게 해야 <br>, <strong> 등 서식용 태그가 그대로 동작한다.
43
+ if (supportTextLink && isSafeUrl(supportTextLink)) {
44
+ const safeLink = escapeHtmlAttr(supportTextLink);
45
+ return `<a href="${safeLink}" class="ncua-full-width-notification__link" rel="noopener noreferrer" target="_blank"><span class="${className}">${supportingText}</span></a>`;
46
+ }
47
+ // unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
48
+ return `<span class="${className}">${supportingText}</span>`;
49
+ }
50
+ export function renderActions(actions, wrapperClass) {
51
+ // 액션이 없으면 빈 문자열 반환
52
+ if (!actions || actions.length === 0) {
53
+ return '';
54
+ }
55
+ const buttonsHtml = actions
56
+ .map((action) => {
57
+ const buttonHtml = `
58
+ <button
59
+ class="ncua-btn ncua-btn--sm ncua-btn--${action.hierarchy || 'text'}"
60
+ data-action="${action.label}-${action.hierarchy}"
61
+ >
62
+ ${action.label}
63
+ </button>`;
64
+ return buttonHtml;
65
+ })
66
+ .join('');
67
+ return `<div class="${wrapperClass}">${buttonsHtml}</div>`;
68
+ }
69
+ // 공통 이벤트 처리
70
+ export function bindNotificationEvents(element, actions, onClose, onRemove) {
71
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 닫기/액션 버튼 분기를 한 핸들러에 묶는 기존 구조를 유지. 분리 리팩토링은 별도 작업 범위.
72
+ element.addEventListener('click', (event) => {
73
+ const target = event.target;
74
+ // 닫기 버튼 클릭 처리
75
+ if (target.matches(`.${CLASS_NAMES.FULL_WIDTH.CLOSE_BUTTON}, .${CLASS_NAMES.FLOATING.CLOSE_BUTTON}`) ||
76
+ target.closest(`.${CLASS_NAMES.FULL_WIDTH.CLOSE_BUTTON}, .${CLASS_NAMES.FLOATING.CLOSE_BUTTON}`) ||
77
+ target.closest(`.${CLASS_NAMES.MESSAGE.CLOSE_BUTTON}`)) {
78
+ onClose?.();
79
+ onRemove?.();
80
+ return;
81
+ }
82
+ // 액션 버튼 클릭 처리
83
+ const actionButton = target.closest('.ncua-btn[data-action]');
84
+ if (actionButton && actions) {
85
+ const actionData = actionButton.getAttribute('data-action');
86
+ if (actionData) {
87
+ let matchedAction = null;
88
+ for (const action of actions) {
89
+ const expectedDataAction = `${action.label}-${action.hierarchy || 'link'}`;
90
+ if (actionData === expectedDataAction) {
91
+ matchedAction = action;
92
+ break;
93
+ }
94
+ }
95
+ if (matchedAction?.onClick) {
96
+ matchedAction.onClick();
97
+ }
98
+ }
99
+ }
100
+ });
101
+ }
102
+ // 자동 닫기 설정
103
+ export function setupAutoClose(autoClose, onClose, onRemove) {
104
+ if (autoClose > 0) {
105
+ return window.setTimeout(() => {
106
+ onClose?.();
107
+ onRemove?.();
108
+ }, autoClose);
109
+ }
110
+ return undefined;
111
+ }
112
+ // Mobile detection utility
113
+ export const isMobile = () => {
114
+ return window.innerWidth <= Number.parseInt(BREAKPOINT.mobile, 10);
115
+ };
@@ -0,0 +1,5 @@
1
+ export type ButtonCloseXSize = 'xs' | 'sm' | 'md' | 'lg';
2
+ export type ButtonCloseXTheme = 'dark' | 'light';
3
+ export declare const SVG_SIZE: Record<ButtonCloseXSize, number>;
4
+ export declare const X_CLOSE_SVG: (size: string) => string;
5
+ export declare function ButtonCloseX(size: ButtonCloseXSize, theme?: ButtonCloseXTheme, additionalClasses?: string, ariaLabel?: string, onClick?: () => void): string;
@@ -0,0 +1,33 @@
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, theme = 'light', additionalClasses = '', ariaLabel = '닫기', onClick) {
15
+ const svgSize = SVG_SIZE[size];
16
+ const buttonId = `close-btn-${Math.random().toString(36).substr(2, 9)}`;
17
+ const buttonHTML = `
18
+ <button type="button" id="${buttonId}" class="ncua-button-close-x ncua-button-close-x--${size} ncua-button-close-x--${theme} ${additionalClasses}" aria-label="${ariaLabel}">
19
+ ${X_CLOSE_SVG(svgSize.toString())}
20
+ </button>
21
+ `;
22
+ // onClick이 제공된 경우 이벤트 바인딩
23
+ if (onClick) {
24
+ // DOM에 추가된 후 이벤트 바인딩을 위해 setTimeout 사용
25
+ setTimeout(() => {
26
+ const button = document.getElementById(buttonId);
27
+ if (button) {
28
+ button.addEventListener('click', onClick);
29
+ }
30
+ }, 0);
31
+ }
32
+ return buttonHTML;
33
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HTML 문자열을 DOMPurify 기본 설정으로 sanitize한다.
3
+ *
4
+ * 제거되는 항목 (기본 설정):
5
+ * - `<script>`, `<iframe>`, `<object>` 등 실행 위험 태그
6
+ * - `onclick`, `onerror` 등 inline 이벤트 핸들러 속성
7
+ * - `href="javascript:..."` 같은 javascript: URL
8
+ *
9
+ * 유지되는 항목:
10
+ * - `<svg>`, `<button>`, `<div>`, `<span>`, `<input>` 등 일반 HTML/SVG 태그
11
+ * - `class`, `style`, `role`, `aria-*`, `data-*` 등 표현용 속성
12
+ * - SVG 표현 속성 (`viewBox`, `stroke`, `fill` 등)
13
+ *
14
+ * 이벤트 바인딩은 addEventListener로 별도 처리할 것.
15
+ */
16
+ export declare function sanitizeHtml(dirty: string): string;
17
+ /**
18
+ * 엘리먼트에 콘텐츠를 안전하게 설정한다.
19
+ * - string: sanitize 후 innerHTML 교체
20
+ * - HTMLElement: 기존 자식 제거 후 appendChild
21
+ */
22
+ export declare function setSafeInnerHTML(element: HTMLElement, content: string | HTMLElement): void;
@@ -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
+ }
@@ -19,7 +19,7 @@ const ActionBar = ({ children, className, position = 'top', align = 'space-betwe
19
19
  'ncua-data-grid__action-bar--space-between': align === 'space-between',
20
20
  }), ...rest, children: children }));
21
21
  ActionBar.displayName = 'DataGrid.ActionBar';
22
- const DataGridTable = forwardRef(({ children, className, type = 'horizontal', fixedHeader, maxHeight, hoverable, selectable }, ref) => (_jsx("div", { ref: ref, className: classNames('ncua-data-grid__table', className), children: _jsx(NcuaTable, { className: "ncua-table--in-data-grid", type: type, fixedHeader: fixedHeader, maxHeight: maxHeight, hoverable: hoverable, selectable: selectable, children: children }) })));
22
+ const DataGridTable = forwardRef(({ children, className, type = 'horizontal', fixedHeader, maxHeight, hoverable, selectable, horizontalScroll, minWidth, }, ref) => (_jsx("div", { ref: ref, className: classNames('ncua-data-grid__table', className), children: _jsx(NcuaTable, { className: "ncua-table--in-data-grid", type: type, fixedHeader: fixedHeader, maxHeight: maxHeight, hoverable: hoverable, selectable: selectable, horizontalScroll: horizontalScroll, minWidth: minWidth, children: children }) })));
23
23
  DataGridTable.displayName = 'DataGrid.Table';
24
24
  const Pagination = ({ children, className, ...rest }) => (_jsx("div", { className: classNames('ncua-data-grid__pagination', className), ...rest, children: children }));
25
25
  Pagination.displayName = 'DataGrid.Pagination';
@@ -29,6 +29,13 @@ export type DataGridTableProps = {
29
29
  maxHeight?: string | number;
30
30
  hoverable?: boolean;
31
31
  selectable?: boolean;
32
+ /**
33
+ * 가로 스크롤 활성화 시 외부 wrapper에 overflow-x: auto를 적용하고,
34
+ * 내부의 SelectBox·Dropdown이 FloatingProvider를 통해 자동으로 Portal 렌더로 전환된다.
35
+ */
36
+ horizontalScroll?: boolean;
37
+ /** 가로 스크롤 트리거 임계 너비. horizontalScroll=true 일 때만 의미가 있다. */
38
+ minWidth?: string | number;
32
39
  };
33
40
  export type DataGridPaginationProps = ComponentProps<'div'> & {
34
41
  children: ReactNode;
@@ -6,6 +6,8 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
6
6
  maxHeight?: string | number | undefined;
7
7
  hoverable?: boolean | undefined;
8
8
  selectable?: boolean | undefined;
9
+ horizontalScroll?: boolean | undefined;
10
+ minWidth?: string | number | undefined;
9
11
  children: ReactNode;
10
12
  } & import("react").RefAttributes<HTMLDivElement>> & {
11
13
  Header: {
@@ -24,6 +26,7 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
24
26
  sortDirection?: SortDirection | undefined;
25
27
  onSort?: (() => void) | undefined;
26
28
  width?: string | number | undefined;
29
+ minWidth?: string | number | undefined;
27
30
  } & import("react").RefAttributes<HTMLTableCellElement>>;
28
31
  Cell: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement>, "ref"> & {
29
32
  isHeader?: boolean | undefined;
@@ -37,7 +40,7 @@ export declare const Table: import("react").ForwardRefExoticComponent<Omit<impor
37
40
  displayName: string;
38
41
  };
39
42
  ColGroup: {
40
- ({ widths }: TableColGroupProps): import("react/jsx-runtime").JSX.Element;
43
+ ({ widths, minWidths }: TableColGroupProps): import("react/jsx-runtime").JSX.Element;
41
44
  displayName: string;
42
45
  };
43
46
  Empty: {
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown, ChevronSelectorVertical, ChevronUp } from '@ncds/ui-admin-icon';
3
3
  import classNames from 'classnames';
4
- import { Children, forwardRef, useEffect, useRef, } from 'react';
5
- // ──────────────────────────────────────────────
6
- // $table-header-height 동기화 sticky thead가 차지하는 높이를 maxHeight에 보상
7
- const TABLE_HEADER_HEIGHT = 40;
8
- // 스크롤바 트랙 상하 여백 합계 (top 8px + bottom 8px) — SCSS &--fixed-header &__scrollbar 오프셋과 동기화
9
- const SCROLLBAR_TRACK_OFFSET = 16;
10
- const SCROLLBAR_THUMB_MIN_HEIGHT = 40;
4
+ import { Children, forwardRef, useRef, } from 'react';
5
+ import { FloatingProvider } from '../../../contexts/FloatingContext';
6
+ import { TABLE_HEADER_HEIGHT, useTableHorizontalScrollbar, useTableVerticalScrollbar } from './useTableScrollbars';
7
+ // 가로 스크롤 디자인 기준 폭 — 14인치 모니터 + LNB 고려한 디자인 권장 너비
8
+ const DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH = 1140;
9
+ const FLOATING_PORTAL_VALUE = { preferPortal: true };
10
+ // TABLE_HEADER_HEIGHT·DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH를 CSS 커스텀 프로퍼티로 주입 — SCSS fallback 단일 소스
11
+ const WRAPPER_STYLE = {
12
+ '--ncua-table-header-height': `${TABLE_HEADER_HEIGHT}px`,
13
+ '--ncua-table-default-min-width': `${DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH}px`,
14
+ };
11
15
  // Sort Icons (@ncds/ui-admin-icon)
12
16
  // ──────────────────────────────────────────────
13
17
  const SORT_ICONS = {
@@ -33,12 +37,12 @@ const Row = forwardRef(({ children, className, selected, status, ...rest }, ref)
33
37
  'ncua-table__row--error': status === 'error',
34
38
  }), ...rest, children: children })));
35
39
  Row.displayName = 'Table.Row';
36
- const HeaderCell = forwardRef(({ children, className, sortDirection, onSort, width, style, ...rest }, ref) => {
40
+ const HeaderCell = forwardRef(({ children, className, sortDirection, onSort, width, minWidth, style, ...rest }, ref) => {
37
41
  const isSortable = sortDirection !== undefined && onSort !== undefined;
38
42
  const SortIcon = isSortable ? SORT_ICONS[sortDirection] : undefined;
39
43
  return (_jsx("th", { ref: ref, className: classNames('ncua-table__header-cell', className, {
40
44
  'ncua-table__header-cell--sortable': isSortable,
41
- }), style: { ...style, width }, "aria-sort": isSortable ? ARIA_SORT_MAP[sortDirection] : undefined, onClick: isSortable ? onSort : undefined, ...rest, children: isSortable && SortIcon ? (_jsxs("span", { className: "ncua-table__header-cell-inner", children: [_jsx("span", { className: "ncua-table__header-cell-text", children: children }), _jsx("span", { className: "ncua-table__sort-icon", children: _jsx(SortIcon, { width: 16, height: 16 }) })] })) : (children) }));
45
+ }), style: { ...style, width, minWidth }, "aria-sort": isSortable ? ARIA_SORT_MAP[sortDirection] : undefined, onClick: isSortable ? onSort : undefined, ...rest, children: isSortable && SortIcon ? (_jsxs("span", { className: "ncua-table__header-cell-inner", children: [_jsx("span", { className: "ncua-table__header-cell-text", children: children }), _jsx("span", { className: "ncua-table__sort-icon", children: _jsx(SortIcon, { width: 16, height: 16 }) })] })) : (children) }));
42
46
  });
43
47
  HeaderCell.displayName = 'Table.HeaderCell';
44
48
  const Cell = forwardRef(({ children, className, isHeader, ...rest }, ref) => {
@@ -52,9 +56,9 @@ const Footer = ({ children, className }) => (_jsx("div", { className: classNames
52
56
  Footer.displayName = 'Table.Footer';
53
57
  const Pagination = ({ children, className }) => (_jsx("div", { className: classNames('ncua-table__pagination', className), children: children }));
54
58
  Pagination.displayName = 'Table.Pagination';
55
- const ColGroup = ({ widths }) => {
59
+ const ColGroup = ({ widths, minWidths }) => {
56
60
  const resolveColWidth = (width) => {
57
- if (width === 'auto')
61
+ if (width === undefined || width === 'auto')
58
62
  return undefined;
59
63
  if (typeof width === 'number')
60
64
  return `${width}px`;
@@ -62,7 +66,7 @@ const ColGroup = ({ widths }) => {
62
66
  };
63
67
  return (_jsx("colgroup", { children: widths.map((width, index) => (
64
68
  // biome-ignore lint/suspicious/noArrayIndexKey: colgroup columns never reorder or change
65
- _jsx("col", { style: { width: resolveColWidth(width) } }, index))) }));
69
+ _jsx("col", { style: { width: resolveColWidth(width), minWidth: resolveColWidth(minWidths?.[index]) } }, index))) }));
66
70
  };
67
71
  ColGroup.displayName = 'Table.ColGroup';
68
72
  const Empty = ({ colSpan, children }) => (_jsx("tr", { children: _jsx("td", { colSpan: colSpan, className: "ncua-table__empty", role: "status", "aria-live": "polite", children: children || '등록된 게시물이 없습니다.' }) }));
@@ -104,7 +108,7 @@ const sortChildren = (children) => {
104
108
  // ──────────────────────────────────────────────
105
109
  // Main Table component
106
110
  // ──────────────────────────────────────────────
107
- const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, maxHeight, hoverable = true, selectable = false, children, className, ...rest }, ref) => {
111
+ const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, maxHeight, hoverable = true, selectable = false, horizontalScroll = false, minWidth, children, className, ...rest }, ref) => {
108
112
  const tableClasses = classNames('ncua-table', className, {
109
113
  'ncua-table--horizontal': type === 'horizontal',
110
114
  'ncua-table--vertical': type === 'vertical',
@@ -123,64 +127,45 @@ const TableComponent = forwardRef(({ type = 'horizontal', fixedHeader = false, m
123
127
  // Custom scrollbar refs (used only in fixed-header mode)
124
128
  const scrollContainerRef = useRef(null);
125
129
  const scrollAreaRef = useRef(null);
130
+ const scrollbarRef = useRef(null);
126
131
  const thumbRef = useRef(null);
127
- useEffect(() => {
128
- if (!fixedHeader || !maxHeight)
129
- return;
130
- const scrollEl = scrollContainerRef.current;
131
- const thumbEl = thumbRef.current;
132
- if (!scrollEl || !thumbEl)
133
- return;
134
- const update = () => {
135
- const { scrollTop, scrollHeight, clientHeight } = scrollEl;
136
- if (scrollHeight <= clientHeight) {
137
- thumbEl.style.height = '0';
138
- return;
139
- }
140
- const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
141
- const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, (clientHeight / scrollHeight) * trackHeight);
142
- const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
143
- thumbEl.style.height = `${thumbHeight}px`;
144
- thumbEl.style.transform = `translateY(${thumbTop}px)`;
145
- };
146
- scrollEl.addEventListener('scroll', update, { passive: true });
147
- const observer = new ResizeObserver(update);
148
- observer.observe(scrollEl);
149
- update();
150
- return () => {
151
- scrollEl.removeEventListener('scroll', update);
152
- observer.disconnect();
153
- };
154
- }, [fixedHeader, maxHeight]);
155
- const handleThumbMouseDown = (e) => {
156
- e.preventDefault();
157
- const scrollEl = scrollContainerRef.current;
158
- const thumbEl = thumbRef.current;
159
- const areaEl = scrollAreaRef.current;
160
- if (!scrollEl || !thumbEl)
161
- return;
162
- areaEl?.setAttribute('data-dragging', '');
163
- const startY = e.clientY;
164
- const startScrollTop = scrollEl.scrollTop;
165
- const { scrollHeight, clientHeight } = scrollEl;
166
- const thumbHeight = thumbEl.offsetHeight;
167
- const scrollRatio = (scrollHeight - clientHeight) / (clientHeight - thumbHeight);
168
- const onMove = (ev) => {
169
- scrollEl.scrollTop = startScrollTop + (ev.clientY - startY) * scrollRatio;
170
- };
171
- const onUp = () => {
172
- areaEl?.removeAttribute('data-dragging');
173
- document.removeEventListener('mousemove', onMove);
174
- document.removeEventListener('mouseup', onUp);
132
+ // 가로 스크롤바 refs (horizontalScroll 모드)
133
+ const hScrollContainerRef = useRef(null);
134
+ const hScrollbarRef = useRef(null);
135
+ const hThumbRef = useRef(null);
136
+ const fixedScrollEnabled = !!(fixedHeader && maxHeight);
137
+ const { handleThumbMouseDown } = useTableVerticalScrollbar({
138
+ enabled: fixedScrollEnabled,
139
+ scrollContainerRef,
140
+ scrollAreaRef,
141
+ thumbRef,
142
+ });
143
+ const { handleHThumbMouseDown } = useTableHorizontalScrollbar({
144
+ enabled: horizontalScroll,
145
+ hScrollContainerRef,
146
+ hScrollbarRef,
147
+ hThumbRef,
148
+ });
149
+ // <colgroup> + <thead> + <tbody> 묶음 — fixed-header 분기와 horizontalScroll 분기 모두에서 재사용
150
+ const renderTable = () => (_jsxs("table", { className: "ncua-table__table", role: "table", children: [colGroupContent, headerContent, tableContent] }));
151
+ // fixed-header 시 scroll-area + scrollbar 래핑, 아니면 <table> 그대로.
152
+ // withScrollbar=false 이면 scrollbar를 제외 — horizontalScroll 분기에서 scrollbar를
153
+ // h-scroll-container 형제 위치에 별도 렌더해 가로 스크롤 시 viewport 우측에 자연 고정.
154
+ const renderScrollableArea = (includeVerticalScrollbar = true) => fixedScrollEnabled ? (_jsxs("div", { ref: scrollAreaRef, className: "ncua-table__scroll-area", children: [_jsx("div", { ref: scrollContainerRef, className: "ncua-table__scroll-container", style: scrollStyle, children: renderTable() }), includeVerticalScrollbar && (_jsx("div", { ref: scrollbarRef, className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) }))] })) : (renderTable());
155
+ // horizontalScroll=true 외곽 wrapper + FloatingProvider 부착.
156
+ // 핵심 — __h-scroll-container 는 <table>(또는 scroll-area) 만 감싸고, footer/pagination 은
157
+ // 그 바깥에서 항상 고정 위치. 세로 스크롤바는 h-scroll-container 형제로 배치되어
158
+ // 가로 스크롤에 영향받지 않고 .ncua-table 우측에 absolute 고정된다.
159
+ if (horizontalScroll) {
160
+ const resolvedMinWidth = minWidth ?? DEFAULT_HORIZONTAL_SCROLL_MIN_WIDTH;
161
+ // CSS 변수로 전달 — SCSS 에서 max(100%, var(--ncua-table-min-width)) 로 부모 너비를 항상 보장한다.
162
+ // (inline min-width 를 직접 주면 부모보다 작은 값에서 wrapper 가 좁아져 콘텐츠가 깨짐)
163
+ const innerStyle = {
164
+ '--ncua-table-min-width': typeof resolvedMinWidth === 'number' ? `${resolvedMinWidth}px` : resolvedMinWidth,
175
165
  };
176
- document.addEventListener('mousemove', onMove);
177
- document.addEventListener('mouseup', onUp);
178
- };
179
- if (fixedHeader && maxHeight) {
180
- return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", children: [_jsxs("div", { className: tableClasses, ...rest, children: [_jsxs("div", { ref: scrollAreaRef, className: "ncua-table__scroll-area", children: [_jsx("div", { ref: scrollContainerRef, className: "ncua-table__scroll-container", style: scrollStyle, children: _jsxs("table", { className: "ncua-table__table", role: "table", children: [colGroupContent, headerContent, tableContent] }) }), _jsx("div", { className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) })] }), footerContent] }), paginationContent] }));
166
+ return (_jsx(FloatingProvider, { value: FLOATING_PORTAL_VALUE, children: _jsxs("div", { ref: ref, className: "ncua-table-wrapper", style: WRAPPER_STYLE, children: [_jsxs("div", { className: tableClasses, ...rest, children: [_jsxs("div", { ref: hScrollContainerRef, className: "ncua-table__h-scroll-container", children: [_jsx("div", { className: "ncua-table__h-scroll-inner", style: innerStyle, children: renderScrollableArea(false) }), _jsx("div", { ref: hScrollbarRef, className: "ncua-table__h-scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: hThumbRef, className: "ncua-table__h-scrollbar-thumb", onMouseDown: handleHThumbMouseDown }) })] }), fixedScrollEnabled && (_jsx("div", { ref: scrollbarRef, className: "ncua-table__scrollbar", "aria-hidden": "true", children: _jsx("div", { ref: thumbRef, className: "ncua-table__scrollbar-thumb", onMouseDown: handleThumbMouseDown }) })), footerContent] }), paginationContent] }) }));
181
167
  }
182
- const tableElement = (_jsxs("table", { className: "ncua-table__table", role: "table", children: [colGroupContent, headerContent, tableContent] }));
183
- return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", children: [_jsxs("div", { className: tableClasses, ...rest, children: [tableElement, footerContent] }), paginationContent] }));
168
+ return (_jsxs("div", { ref: ref, className: "ncua-table-wrapper", style: WRAPPER_STYLE, children: [_jsxs("div", { className: tableClasses, ...rest, children: [renderScrollableArea(), footerContent] }), paginationContent] }));
184
169
  });
185
170
  TableComponent.displayName = 'Table';
186
171
  // ──────────────────────────────────────────────