@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.
- package/dist/cjs/assets/scripts/featuredIcon.js +87 -0
- package/dist/cjs/assets/scripts/notification/FloatingNotification.js +178 -0
- package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +133 -0
- package/dist/cjs/assets/scripts/notification/MessageNotification.js +159 -0
- package/dist/cjs/assets/scripts/notification/Notification.js +120 -0
- package/dist/cjs/assets/scripts/notification/const/classNames.js +50 -0
- package/dist/cjs/assets/scripts/notification/const/icons.js +31 -0
- package/dist/cjs/assets/scripts/notification/const/index.js +87 -0
- package/dist/cjs/assets/scripts/notification/const/sizes.js +46 -0
- package/dist/cjs/assets/scripts/notification/const/types.js +14 -0
- package/dist/cjs/assets/scripts/notification/index.js +116 -0
- package/dist/cjs/assets/scripts/notification/positionSync.js +180 -0
- package/dist/cjs/assets/scripts/notification/utils.js +122 -0
- package/dist/cjs/assets/scripts/shared/ButtonCloseX.js +45 -0
- package/dist/cjs/assets/scripts/utils/sanitize.js +39 -0
- package/dist/cjs/src/components/data-display/data-grid/DataGrid.js +5 -1
- package/dist/cjs/src/components/data-display/table/Table.js +118 -96
- package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +187 -0
- package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
- package/dist/cjs/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
- package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +67 -29
- package/dist/cjs/src/components/index.js +33 -0
- package/dist/cjs/src/components/layout/block-container/BlockContainer.js +38 -0
- package/dist/cjs/src/components/layout/block-container/index.js +16 -0
- package/dist/cjs/src/components/layout/block-header/BlockHeader.js +107 -0
- package/dist/cjs/src/components/layout/block-header/SubTitle.js +56 -0
- package/dist/cjs/src/components/layout/block-header/index.js +27 -0
- package/dist/cjs/src/components/layout/page-title/PageTitle.js +95 -0
- package/dist/cjs/src/components/layout/page-title/index.js +16 -0
- package/dist/cjs/src/components/overlays/dropdown/Dropdown.js +47 -19
- package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +25 -0
- package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +86 -13
- package/dist/cjs/src/components/overlays/notification/Notification.js +7 -0
- package/dist/cjs/src/components/overlays/notification/host.js +12 -0
- package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +57 -44
- package/dist/cjs/src/components/select-dropdown/SelectDropdown.js +2 -1
- package/dist/cjs/src/contexts/FloatingContext.js +11 -0
- package/dist/cjs/src/contexts/index.js +16 -0
- package/dist/cjs/src/hooks/index.js +11 -0
- package/dist/cjs/src/hooks/useFloatingPosition.js +78 -0
- package/dist/cjs/src/hooks/usePortalState.js +17 -0
- package/dist/cjs/src/utils/dropdown/maxSelection.js +35 -0
- package/dist/cjs/src/utils/dropdown/multiSelect.js +72 -15
- package/dist/esm/assets/scripts/featuredIcon.js +80 -0
- package/dist/esm/assets/scripts/notification/FloatingNotification.js +171 -0
- package/dist/esm/assets/scripts/notification/FullWidthNotification.js +126 -0
- package/dist/esm/assets/scripts/notification/MessageNotification.js +152 -0
- package/dist/esm/assets/scripts/notification/Notification.js +113 -0
- package/dist/esm/assets/scripts/notification/const/classNames.js +44 -0
- package/dist/esm/assets/scripts/notification/const/icons.js +25 -0
- package/dist/esm/assets/scripts/notification/const/index.js +4 -0
- package/dist/esm/assets/scripts/notification/const/sizes.js +40 -0
- package/dist/esm/assets/scripts/notification/const/types.js +8 -0
- package/dist/esm/assets/scripts/notification/index.js +10 -0
- package/dist/esm/assets/scripts/notification/positionSync.js +171 -0
- package/dist/esm/assets/scripts/notification/utils.js +109 -0
- package/dist/esm/assets/scripts/shared/ButtonCloseX.js +37 -0
- package/dist/esm/assets/scripts/utils/sanitize.js +31 -0
- package/dist/esm/src/components/data-display/data-grid/DataGrid.js +5 -1
- package/dist/esm/src/components/data-display/table/Table.js +118 -96
- package/dist/esm/src/components/data-display/table/useTableScrollbars.js +179 -0
- package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +11 -10
- package/dist/esm/src/components/forms-and-input/image-file-input/ImageFileInput.js +5 -2
- package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +67 -29
- package/dist/esm/src/components/index.js +3 -0
- package/dist/esm/src/components/layout/block-container/BlockContainer.js +31 -0
- package/dist/esm/src/components/layout/block-container/index.js +1 -0
- package/dist/esm/src/components/layout/block-header/BlockHeader.js +100 -0
- package/dist/esm/src/components/layout/block-header/SubTitle.js +49 -0
- package/dist/esm/src/components/layout/block-header/index.js +2 -0
- package/dist/esm/src/components/layout/page-title/PageTitle.js +88 -0
- package/dist/esm/src/components/layout/page-title/index.js +1 -0
- package/dist/esm/src/components/overlays/dropdown/Dropdown.js +47 -19
- package/dist/esm/src/components/overlays/notification/CalloutNotification.js +19 -0
- package/dist/esm/src/components/overlays/notification/FloatingNotification.js +86 -14
- package/dist/esm/src/components/overlays/notification/Notification.js +7 -0
- package/dist/esm/src/components/overlays/notification/host.js +9 -0
- package/dist/esm/src/components/overlays/tooltip/Tooltip.js +58 -45
- package/dist/esm/src/components/select-dropdown/SelectDropdown.js +2 -1
- package/dist/esm/src/contexts/FloatingContext.js +4 -0
- package/dist/esm/src/contexts/index.js +1 -0
- package/dist/esm/src/hooks/index.js +1 -0
- package/dist/esm/src/hooks/useFloatingPosition.js +71 -0
- package/dist/esm/src/hooks/usePortalState.js +10 -0
- package/dist/esm/src/utils/dropdown/maxSelection.js +27 -0
- package/dist/esm/src/utils/dropdown/multiSelect.js +70 -14
- package/dist/temp/assets/scripts/featuredIcon.d.ts +22 -0
- package/dist/temp/assets/scripts/featuredIcon.js +79 -0
- package/dist/temp/assets/scripts/notification/FloatingNotification.d.ts +24 -0
- package/dist/temp/assets/scripts/notification/FloatingNotification.js +156 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.js +111 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +22 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.js +140 -0
- package/dist/temp/assets/scripts/notification/Notification.d.ts +22 -0
- package/dist/temp/assets/scripts/notification/Notification.js +112 -0
- package/dist/temp/assets/scripts/notification/const/classNames.d.ts +43 -0
- package/dist/temp/assets/scripts/notification/const/classNames.js +44 -0
- package/dist/temp/assets/scripts/notification/const/icons.d.ts +25 -0
- package/dist/temp/assets/scripts/notification/const/icons.js +25 -0
- package/dist/temp/assets/scripts/notification/const/index.d.ts +5 -0
- package/dist/temp/assets/scripts/notification/const/index.js +4 -0
- package/dist/temp/assets/scripts/notification/const/sizes.d.ts +32 -0
- package/dist/temp/assets/scripts/notification/const/sizes.js +40 -0
- package/dist/temp/assets/scripts/notification/const/types.d.ts +19 -0
- package/dist/temp/assets/scripts/notification/const/types.js +8 -0
- package/dist/temp/assets/scripts/notification/index.d.ts +8 -0
- package/dist/temp/assets/scripts/notification/index.js +10 -0
- package/dist/temp/assets/scripts/notification/positionSync.d.ts +50 -0
- package/dist/temp/assets/scripts/notification/positionSync.js +170 -0
- package/dist/temp/assets/scripts/notification/utils.d.ts +8 -0
- package/dist/temp/assets/scripts/notification/utils.js +115 -0
- package/dist/temp/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
- package/dist/temp/assets/scripts/shared/ButtonCloseX.js +33 -0
- package/dist/temp/assets/scripts/utils/sanitize.d.ts +22 -0
- package/dist/temp/assets/scripts/utils/sanitize.js +31 -0
- package/dist/temp/src/components/data-display/data-grid/DataGrid.js +1 -1
- package/dist/temp/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
- package/dist/temp/src/components/data-display/table/Table.d.ts +4 -1
- package/dist/temp/src/components/data-display/table/Table.js +53 -68
- package/dist/temp/src/components/data-display/table/types.d.ts +18 -0
- package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
- package/dist/temp/src/components/data-display/table/useTableScrollbars.js +136 -0
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +7 -11
- package/dist/temp/src/components/forms-and-input/image-file-input/ImageFileInput.js +1 -1
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +30 -3
- package/dist/temp/src/components/index.d.ts +3 -0
- package/dist/temp/src/components/index.js +3 -0
- package/dist/temp/src/components/layout/block-container/BlockContainer.d.ts +19 -0
- package/dist/temp/src/components/layout/block-container/BlockContainer.js +11 -0
- package/dist/temp/src/components/layout/block-container/index.d.ts +1 -0
- package/dist/temp/src/components/layout/block-container/index.js +1 -0
- package/dist/temp/src/components/layout/block-header/BlockHeader.d.ts +23 -0
- package/dist/temp/src/components/layout/block-header/BlockHeader.js +21 -0
- package/dist/temp/src/components/layout/block-header/SubTitle.d.ts +19 -0
- package/dist/temp/src/components/layout/block-header/SubTitle.js +8 -0
- package/dist/temp/src/components/layout/block-header/index.d.ts +2 -0
- package/dist/temp/src/components/layout/block-header/index.js +2 -0
- package/dist/temp/src/components/layout/page-title/PageTitle.d.ts +22 -0
- package/dist/temp/src/components/layout/page-title/PageTitle.js +19 -0
- package/dist/temp/src/components/layout/page-title/index.d.ts +1 -0
- package/dist/temp/src/components/layout/page-title/index.js +1 -0
- package/dist/temp/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
- package/dist/temp/src/components/overlays/dropdown/Dropdown.js +35 -11
- package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
- package/dist/temp/src/components/overlays/notification/CalloutNotification.js +6 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
- package/dist/temp/src/components/overlays/notification/FloatingNotification.js +81 -13
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +18 -3
- package/dist/temp/src/components/overlays/notification/Notification.js +4 -0
- package/dist/temp/src/components/overlays/notification/host.d.ts +9 -0
- package/dist/temp/src/components/overlays/notification/host.js +9 -0
- package/dist/temp/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
- package/dist/temp/src/components/overlays/tooltip/Tooltip.js +25 -22
- package/dist/temp/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
- package/dist/temp/src/components/select-dropdown/SelectDropdown.js +2 -2
- package/dist/temp/src/contexts/FloatingContext.d.ts +6 -0
- package/dist/temp/src/contexts/FloatingContext.js +4 -0
- package/dist/temp/src/contexts/index.d.ts +1 -0
- package/dist/temp/src/contexts/index.js +1 -0
- package/dist/temp/src/hooks/index.d.ts +1 -0
- package/dist/temp/src/hooks/index.js +1 -0
- package/dist/temp/src/hooks/useFloatingPosition.d.ts +19 -0
- package/dist/temp/src/hooks/useFloatingPosition.js +55 -0
- package/dist/temp/src/hooks/usePortalState.d.ts +6 -0
- package/dist/temp/src/hooks/usePortalState.js +7 -0
- package/dist/temp/src/utils/dropdown/maxSelection.d.ts +24 -0
- package/dist/temp/src/utils/dropdown/maxSelection.js +28 -0
- package/dist/temp/src/utils/dropdown/multiSelect.d.ts +42 -2
- package/dist/temp/src/utils/dropdown/multiSelect.js +66 -13
- package/dist/types/assets/scripts/featuredIcon.d.ts +22 -0
- package/dist/types/assets/scripts/notification/FloatingNotification.d.ts +24 -0
- package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +21 -0
- package/dist/types/assets/scripts/notification/MessageNotification.d.ts +22 -0
- package/dist/types/assets/scripts/notification/Notification.d.ts +22 -0
- package/dist/types/assets/scripts/notification/const/classNames.d.ts +43 -0
- package/dist/types/assets/scripts/notification/const/icons.d.ts +25 -0
- package/dist/types/assets/scripts/notification/const/index.d.ts +5 -0
- package/dist/types/assets/scripts/notification/const/sizes.d.ts +32 -0
- package/dist/types/assets/scripts/notification/const/types.d.ts +19 -0
- package/dist/types/assets/scripts/notification/index.d.ts +8 -0
- package/dist/types/assets/scripts/notification/positionSync.d.ts +50 -0
- package/dist/types/assets/scripts/notification/utils.d.ts +8 -0
- package/dist/types/assets/scripts/shared/ButtonCloseX.d.ts +5 -0
- package/dist/types/assets/scripts/utils/sanitize.d.ts +22 -0
- package/dist/types/src/components/data-display/data-grid/DataGrid.types.d.ts +7 -0
- package/dist/types/src/components/data-display/table/Table.d.ts +4 -1
- package/dist/types/src/components/data-display/table/types.d.ts +18 -0
- package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +25 -0
- package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +8 -0
- package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +13 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/layout/block-container/BlockContainer.d.ts +19 -0
- package/dist/types/src/components/layout/block-container/index.d.ts +1 -0
- package/dist/types/src/components/layout/block-header/BlockHeader.d.ts +23 -0
- package/dist/types/src/components/layout/block-header/SubTitle.d.ts +19 -0
- package/dist/types/src/components/layout/block-header/index.d.ts +2 -0
- package/dist/types/src/components/layout/page-title/PageTitle.d.ts +22 -0
- package/dist/types/src/components/layout/page-title/index.d.ts +1 -0
- package/dist/types/src/components/overlays/dropdown/Dropdown.d.ts +5 -0
- package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +9 -0
- package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +15 -0
- package/dist/types/src/components/overlays/notification/Notification.d.ts +18 -3
- package/dist/types/src/components/overlays/notification/host.d.ts +9 -0
- package/dist/types/src/components/overlays/tooltip/Tooltip.d.ts +5 -1
- package/dist/types/src/components/select-dropdown/SelectDropdown.d.ts +6 -0
- package/dist/types/src/contexts/FloatingContext.d.ts +6 -0
- package/dist/types/src/contexts/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/useFloatingPosition.d.ts +19 -0
- package/dist/types/src/hooks/usePortalState.d.ts +6 -0
- package/dist/types/src/utils/dropdown/maxSelection.d.ts +24 -0
- package/dist/types/src/utils/dropdown/multiSelect.d.ts +42 -2
- package/dist/ui-admin/assets/styles/style.css +596 -64
- package/package.json +1 -1
|
@@ -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, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
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,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ──────────────────────────────────────────────
|
|
@@ -8,6 +8,17 @@ export type TableProps = Omit<ComponentProps<'div'>, 'ref'> & {
|
|
|
8
8
|
maxHeight?: string | number;
|
|
9
9
|
hoverable?: boolean;
|
|
10
10
|
selectable?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 가로 스크롤 활성화. 외곽에 overflow-x: auto wrapper가 추가되며,
|
|
13
|
+
* 내부의 SelectBox·Dropdown 등 floating 컴포넌트가 FloatingProvider를 통해
|
|
14
|
+
* 자동으로 React Portal 렌더로 전환된다.
|
|
15
|
+
*/
|
|
16
|
+
horizontalScroll?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 가로 스크롤 트리거 임계 너비. horizontalScroll=true 일 때만 의미가 있으며,
|
|
19
|
+
* 기본값은 1140 (14인치 모니터 + LNB 기준 디자인 권장 너비).
|
|
20
|
+
*/
|
|
21
|
+
minWidth?: string | number;
|
|
11
22
|
children: ReactNode;
|
|
12
23
|
};
|
|
13
24
|
export type TableHeaderProps = {
|
|
@@ -26,6 +37,11 @@ export type TableHeaderCellProps = Omit<ComponentProps<'th'>, 'ref'> & {
|
|
|
26
37
|
sortDirection?: SortDirection;
|
|
27
38
|
onSort?: () => void;
|
|
28
39
|
width?: string | number;
|
|
40
|
+
/**
|
|
41
|
+
* 셀 최소 너비. 가로 스크롤 정책에 따라 글자수 기준으로 설정한다.
|
|
42
|
+
* 예: 제목·메인 10자 ≈ 160px, 이름·일자 5자 ≈ 80px, 버튼 xxs ≈ 40~60px, 기타 2자 ≈ 32px.
|
|
43
|
+
*/
|
|
44
|
+
minWidth?: string | number;
|
|
29
45
|
};
|
|
30
46
|
export type TableCellProps = Omit<ComponentProps<'td'>, 'ref'> & {
|
|
31
47
|
isHeader?: boolean;
|
|
@@ -40,6 +56,8 @@ export type TablePaginationProps = {
|
|
|
40
56
|
};
|
|
41
57
|
export type TableColGroupProps = {
|
|
42
58
|
widths: (string | number)[];
|
|
59
|
+
/** 컬럼별 최소 너비. 가로 스크롤 시 셀이 더 좁아지지 않도록 강제한다. */
|
|
60
|
+
minWidths?: (string | number)[];
|
|
43
61
|
};
|
|
44
62
|
export type TableEmptyProps = {
|
|
45
63
|
colSpan: number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type MouseEvent, type RefObject } from 'react';
|
|
2
|
+
export declare const TABLE_HEADER_HEIGHT = 40;
|
|
3
|
+
export declare const SCROLLBAR_THUMB_MIN_HEIGHT = 40;
|
|
4
|
+
export declare const H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
|
|
5
|
+
export declare const H_SCROLLBAR_SIDE_GAP = 8;
|
|
6
|
+
export declare const SCROLLBAR_TRACK_OFFSET = 16;
|
|
7
|
+
type VerticalScrollbarOptions = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
scrollContainerRef: RefObject<HTMLDivElement | null>;
|
|
10
|
+
scrollAreaRef: RefObject<HTMLDivElement | null>;
|
|
11
|
+
thumbRef: RefObject<HTMLDivElement | null>;
|
|
12
|
+
};
|
|
13
|
+
export declare const useTableVerticalScrollbar: ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }: VerticalScrollbarOptions) => {
|
|
14
|
+
handleThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
|
|
15
|
+
};
|
|
16
|
+
type HorizontalScrollbarOptions = {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
hScrollContainerRef: RefObject<HTMLDivElement | null>;
|
|
19
|
+
hScrollbarRef: RefObject<HTMLDivElement | null>;
|
|
20
|
+
hThumbRef: RefObject<HTMLDivElement | null>;
|
|
21
|
+
};
|
|
22
|
+
export declare const useTableHorizontalScrollbar: ({ enabled, hScrollContainerRef, hScrollbarRef, hThumbRef, }: HorizontalScrollbarOptions) => {
|
|
23
|
+
handleHThumbMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
// ──────────────────────────────────────────────
|
|
3
|
+
// 상수 — Table.tsx 와 _table.scss 양쪽에서 동기화 필요
|
|
4
|
+
// ──────────────────────────────────────────────
|
|
5
|
+
// $table-header-height
|
|
6
|
+
export const TABLE_HEADER_HEIGHT = 40;
|
|
7
|
+
// 세로/가로 thumb 최소 크기
|
|
8
|
+
export const SCROLLBAR_THUMB_MIN_HEIGHT = 40;
|
|
9
|
+
export const H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
|
|
10
|
+
// SCSS .ncua-table__h-scrollbar { left/right: var(--spacing-s) = 8px } 와 동기화
|
|
11
|
+
export const H_SCROLLBAR_SIDE_GAP = 8;
|
|
12
|
+
// 세로 트랙 상하 여백 합계 — top 8 + bottom 8 = 16 (header 회피분 40 은 별도 처리)
|
|
13
|
+
// biome-ignore lint/style/useExportsLast: 상수는 문서 주석과 함께 상단에 정의
|
|
14
|
+
export const SCROLLBAR_TRACK_OFFSET = 16;
|
|
15
|
+
const startDrag = (e, options) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
const { axis, scrollEl, thumbEl, draggingTarget = scrollEl, sideGap = 0 } = options;
|
|
18
|
+
draggingTarget.setAttribute('data-dragging', '');
|
|
19
|
+
if (axis === 'y') {
|
|
20
|
+
const startY = e.clientY;
|
|
21
|
+
const startScrollTop = scrollEl.scrollTop;
|
|
22
|
+
const { scrollHeight, clientHeight } = scrollEl;
|
|
23
|
+
const thumbHeight = thumbEl.offsetHeight;
|
|
24
|
+
const ratio = (scrollHeight - clientHeight) / (clientHeight - thumbHeight);
|
|
25
|
+
const onMove = (ev) => {
|
|
26
|
+
scrollEl.scrollTop = startScrollTop + (ev.clientY - startY) * ratio;
|
|
27
|
+
};
|
|
28
|
+
const onUp = () => {
|
|
29
|
+
draggingTarget.removeAttribute('data-dragging');
|
|
30
|
+
document.removeEventListener('mousemove', onMove);
|
|
31
|
+
document.removeEventListener('mouseup', onUp);
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener('mousemove', onMove);
|
|
34
|
+
document.addEventListener('mouseup', onUp);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const startX = e.clientX;
|
|
38
|
+
const startScrollLeft = scrollEl.scrollLeft;
|
|
39
|
+
const { scrollWidth, clientWidth } = scrollEl;
|
|
40
|
+
const thumbWidth = thumbEl.offsetWidth;
|
|
41
|
+
const trackWidth = clientWidth - sideGap * 2;
|
|
42
|
+
const ratio = (scrollWidth - clientWidth) / (trackWidth - thumbWidth);
|
|
43
|
+
const onMove = (ev) => {
|
|
44
|
+
scrollEl.scrollLeft = startScrollLeft + (ev.clientX - startX) * ratio;
|
|
45
|
+
};
|
|
46
|
+
const onUp = () => {
|
|
47
|
+
draggingTarget.removeAttribute('data-dragging');
|
|
48
|
+
document.removeEventListener('mousemove', onMove);
|
|
49
|
+
document.removeEventListener('mouseup', onUp);
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener('mousemove', onMove);
|
|
52
|
+
document.addEventListener('mouseup', onUp);
|
|
53
|
+
};
|
|
54
|
+
export const useTableVerticalScrollbar = ({ enabled, scrollContainerRef, scrollAreaRef, thumbRef, }) => {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled)
|
|
57
|
+
return;
|
|
58
|
+
const scrollEl = scrollContainerRef.current;
|
|
59
|
+
const thumbEl = thumbRef.current;
|
|
60
|
+
if (!scrollEl || !thumbEl)
|
|
61
|
+
return;
|
|
62
|
+
const update = () => {
|
|
63
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
64
|
+
if (scrollHeight <= clientHeight) {
|
|
65
|
+
thumbEl.style.height = '0';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const trackHeight = (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
|
|
69
|
+
const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, (clientHeight / scrollHeight) * trackHeight);
|
|
70
|
+
const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
|
|
71
|
+
thumbEl.style.height = `${thumbHeight}px`;
|
|
72
|
+
thumbEl.style.transform = `translateY(${thumbTop}px)`;
|
|
73
|
+
};
|
|
74
|
+
scrollEl.addEventListener('scroll', update, { passive: true });
|
|
75
|
+
const observer = new ResizeObserver(update);
|
|
76
|
+
observer.observe(scrollEl);
|
|
77
|
+
if (scrollAreaRef.current)
|
|
78
|
+
observer.observe(scrollAreaRef.current);
|
|
79
|
+
update();
|
|
80
|
+
return () => {
|
|
81
|
+
scrollEl.removeEventListener('scroll', update);
|
|
82
|
+
observer.disconnect();
|
|
83
|
+
};
|
|
84
|
+
}, [enabled, scrollContainerRef, scrollAreaRef, thumbRef]);
|
|
85
|
+
const handleThumbMouseDown = (e) => {
|
|
86
|
+
const scrollEl = scrollContainerRef.current;
|
|
87
|
+
const thumbEl = thumbRef.current;
|
|
88
|
+
const areaEl = scrollAreaRef.current;
|
|
89
|
+
if (!scrollEl || !thumbEl)
|
|
90
|
+
return;
|
|
91
|
+
startDrag(e, { axis: 'y', scrollEl, thumbEl, draggingTarget: areaEl ?? scrollEl });
|
|
92
|
+
};
|
|
93
|
+
return { handleThumbMouseDown };
|
|
94
|
+
};
|
|
95
|
+
export const useTableHorizontalScrollbar = ({ enabled, hScrollContainerRef, hScrollbarRef, hThumbRef, }) => {
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!enabled)
|
|
98
|
+
return;
|
|
99
|
+
const hScrollEl = hScrollContainerRef.current;
|
|
100
|
+
const hScrollbarEl = hScrollbarRef.current;
|
|
101
|
+
const hThumbEl = hThumbRef.current;
|
|
102
|
+
if (!hScrollEl || !hScrollbarEl || !hThumbEl)
|
|
103
|
+
return;
|
|
104
|
+
const update = () => {
|
|
105
|
+
const { scrollLeft, scrollWidth, clientWidth } = hScrollEl;
|
|
106
|
+
if (scrollWidth <= clientWidth) {
|
|
107
|
+
hThumbEl.style.width = '0';
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// transform으로 스크롤 오프셋 보정 — reflow 없이 compositor-only 이동
|
|
111
|
+
hScrollbarEl.style.transform = `translateX(${scrollLeft}px)`;
|
|
112
|
+
hScrollbarEl.style.width = `${clientWidth - H_SCROLLBAR_SIDE_GAP * 2}px`;
|
|
113
|
+
const trackWidth = clientWidth - H_SCROLLBAR_SIDE_GAP * 2;
|
|
114
|
+
const thumbWidth = Math.max(H_SCROLLBAR_THUMB_MIN_WIDTH, (clientWidth / scrollWidth) * trackWidth);
|
|
115
|
+
const thumbLeft = (scrollLeft / (scrollWidth - clientWidth)) * (trackWidth - thumbWidth);
|
|
116
|
+
hThumbEl.style.width = `${thumbWidth}px`;
|
|
117
|
+
hThumbEl.style.transform = `translateX(${thumbLeft}px)`;
|
|
118
|
+
};
|
|
119
|
+
hScrollEl.addEventListener('scroll', update, { passive: true });
|
|
120
|
+
const ro = new ResizeObserver(update);
|
|
121
|
+
ro.observe(hScrollEl);
|
|
122
|
+
update();
|
|
123
|
+
return () => {
|
|
124
|
+
hScrollEl.removeEventListener('scroll', update);
|
|
125
|
+
ro.disconnect();
|
|
126
|
+
};
|
|
127
|
+
}, [enabled, hScrollContainerRef, hScrollbarRef, hThumbRef]);
|
|
128
|
+
const handleHThumbMouseDown = (e) => {
|
|
129
|
+
const hScrollEl = hScrollContainerRef.current;
|
|
130
|
+
const hThumbEl = hThumbRef.current;
|
|
131
|
+
if (!hScrollEl || !hThumbEl)
|
|
132
|
+
return;
|
|
133
|
+
startDrag(e, { axis: 'x', scrollEl: hScrollEl, thumbEl: hThumbEl, sideGap: H_SCROLLBAR_SIDE_GAP });
|
|
134
|
+
};
|
|
135
|
+
return { handleHThumbMouseDown };
|
|
136
|
+
};
|