@ehfuse/overlay-scrollbar 1.2.6 → 1.4.0

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/index.js CHANGED
@@ -5,8 +5,299 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var react = require('react');
7
7
 
8
- const OverlayScrollbar = react.forwardRef(({ className = "", style = {}, children, onScroll, scrollbarWidth = 8, // deprecated
9
- thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidth = 16, thumbWidth = 8, thumbMinHeight = 50, trackColor = "rgba(128, 128, 128, 0.1)", thumbColor = "rgba(128, 128, 128, 0.6)", thumbActiveColor = "rgba(128, 128, 128, 0.9)", arrowColor = "rgba(128, 128, 128, 0.6)", arrowActiveColor = "rgba(64, 64, 64, 1.0)", hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
8
+ /**
9
+ * MIT License
10
+ *
11
+ * Copyright (c) 2025 KIM YOUNG JIN (ehfuse@gmail.com)
12
+ *
13
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ * of this software and associated documentation files (the "Software"), to deal
15
+ * in the Software without restriction, including without limitation the rights
16
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ * copies of the Software, and to permit persons to whom the Software is
18
+ * furnished to do so, subject to the following conditions:
19
+ *
20
+ * The above copyright notice and this permission notice shall be included in all
21
+ * copies or substantial portions of the Software.
22
+ *
23
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ * SOFTWARE.
30
+ */
31
+ // 드래그 스크롤을 제외할 클래스들 (자신 또는 부모 요소에서 확인)
32
+ const DEFAULT_EXCLUDE_CLASSES = [
33
+ // 기본 입력 요소들
34
+ "editor",
35
+ "textarea",
36
+ "input",
37
+ "select",
38
+ "textfield",
39
+ "form-control",
40
+ "contenteditable",
41
+ // Material-UI 컴포넌트들
42
+ "MuiInputBase-input",
43
+ "MuiSelect-select",
44
+ "MuiOutlinedInput-input",
45
+ "MuiFilledInput-input",
46
+ "MuiInput-input",
47
+ "MuiFormControl-root",
48
+ "MuiTextField-root",
49
+ "MuiSelect-root",
50
+ "MuiOutlinedInput-root",
51
+ "MuiFilledInput-root",
52
+ "MuiInput-root",
53
+ "MuiAutocomplete-input",
54
+ "MuiDatePicker-input",
55
+ "MuiSlider-thumb",
56
+ "MuiSlider-rail",
57
+ "MuiSlider-track",
58
+ "MuiSlider-mark",
59
+ "MuiSlider-markLabel",
60
+ "MuiSlider-root",
61
+ "MuiSlider-colorPrimary",
62
+ "MuiSlider-sizeMedium",
63
+ "MuiIconButton-root",
64
+ "MuiButton-root",
65
+ "MuiButtonBase-root",
66
+ "MuiTouchRipple-root",
67
+ "MuiCheckbox-root",
68
+ "MuiRadio-root",
69
+ "MuiSwitch-root",
70
+ "PrivateSwitchBase-root",
71
+ // Ant Design 컴포넌트들
72
+ "ant-input",
73
+ "ant-input-affix-wrapper",
74
+ "ant-input-group-addon",
75
+ "ant-input-number",
76
+ "ant-input-number-handler",
77
+ "ant-select",
78
+ "ant-select-selector",
79
+ "ant-select-selection-search",
80
+ "ant-select-dropdown",
81
+ "ant-cascader",
82
+ "ant-cascader-input",
83
+ "ant-picker",
84
+ "ant-picker-input",
85
+ "ant-time-picker",
86
+ "ant-calendar-picker",
87
+ "ant-slider",
88
+ "ant-slider-track",
89
+ "ant-slider-handle",
90
+ "ant-switch",
91
+ "ant-checkbox",
92
+ "ant-checkbox-wrapper",
93
+ "ant-radio",
94
+ "ant-radio-wrapper",
95
+ "ant-rate",
96
+ "ant-upload",
97
+ "ant-upload-drag",
98
+ "ant-form-item",
99
+ "ant-form-item-control",
100
+ "ant-btn",
101
+ "ant-dropdown",
102
+ "ant-dropdown-trigger",
103
+ "ant-menu",
104
+ "ant-menu-item",
105
+ "ant-tooltip",
106
+ "ant-popover",
107
+ "ant-modal",
108
+ "ant-drawer",
109
+ "ant-tree-select",
110
+ "ant-auto-complete",
111
+ "ant-mentions",
112
+ "ant-transfer",
113
+ // Shadcn/ui 컴포넌트들
114
+ "ui-input",
115
+ "ui-textarea",
116
+ "ui-select",
117
+ "ui-select-trigger",
118
+ "ui-select-content",
119
+ "ui-select-item",
120
+ "ui-button",
121
+ "ui-checkbox",
122
+ "ui-radio-group",
123
+ "ui-switch",
124
+ "ui-slider",
125
+ "ui-range-slider",
126
+ "ui-calendar",
127
+ "ui-date-picker",
128
+ "ui-combobox",
129
+ "ui-command",
130
+ "ui-command-input",
131
+ "ui-popover",
132
+ "ui-dialog",
133
+ "ui-sheet",
134
+ "ui-dropdown-menu",
135
+ "ui-context-menu",
136
+ "ui-menubar",
137
+ "ui-navigation-menu",
138
+ "ui-form",
139
+ "ui-form-control",
140
+ "ui-form-item",
141
+ "ui-form-field",
142
+ "ui-label",
143
+ // Radix UI 기본 클래스들 (Shadcn 기반)
144
+ "radix-ui",
145
+ "radix-select",
146
+ "radix-dropdown",
147
+ "radix-dialog",
148
+ "radix-popover",
149
+ "radix-accordion",
150
+ "radix-tabs",
151
+ "radix-slider",
152
+ "radix-switch",
153
+ "radix-checkbox",
154
+ "radix-radio",
155
+ // Quill Editor
156
+ "ql-editor",
157
+ "ql-container",
158
+ "ql-toolbar",
159
+ "ql-picker",
160
+ "ql-picker-label",
161
+ "ql-picker-options",
162
+ "ql-formats",
163
+ "ql-snow",
164
+ "ql-bubble",
165
+ "quill",
166
+ "quilleditor",
167
+ // Monaco Editor
168
+ "monaco-editor",
169
+ "monaco-editor-background",
170
+ "view-lines",
171
+ "decorationsOverviewRuler",
172
+ "monaco-scrollable-element",
173
+ // CodeMirror
174
+ "CodeMirror",
175
+ "CodeMirror-code",
176
+ "CodeMirror-lines",
177
+ "CodeMirror-scroll",
178
+ "CodeMirror-sizer",
179
+ "cm-editor",
180
+ "cm-focused",
181
+ "cm-content",
182
+ // TinyMCE
183
+ "tox-editor-container",
184
+ "tox-editor-header",
185
+ "tox-edit-area",
186
+ "tox-tinymce",
187
+ "mce-content-body",
188
+ // CKEditor
189
+ "ck-editor",
190
+ "ck-content",
191
+ "ck-toolbar",
192
+ "ck-editor__editable",
193
+ "ck-widget",
194
+ // Slate.js
195
+ "slate-editor",
196
+ "slate-content",
197
+ // Draft.js
198
+ "DraftEditor-root",
199
+ "DraftEditor-editorContainer",
200
+ "public-DraftEditor-content",
201
+ // EhfuseEditor
202
+ "ehfuse-editor",
203
+ "ehfuse-editor-wrapper",
204
+ "ehfuse-editor-content",
205
+ "ehfuse-toolbar",
206
+ "ehfuse-toolbar-group",
207
+ "ehfuse-cursor",
208
+ // 기타 에디터들
209
+ "text-editor",
210
+ "rich-text-editor",
211
+ "wysiwyg",
212
+ "ace_editor",
213
+ "ace_content",
214
+ ];
215
+ /**
216
+ * 드래그 스크롤이 허용되지 않는 요소들인지 확인
217
+ */
218
+ /**
219
+ * 드래그 스크롤이 허용되지 않는 요소들인지 확인
220
+ */
221
+ const isTextInputElement = (element, config) => {
222
+ const tagName = element.tagName.toLowerCase();
223
+ const inputTypes = [
224
+ "text",
225
+ "password",
226
+ "email",
227
+ "number",
228
+ "search",
229
+ "tel",
230
+ "url",
231
+ "checkbox",
232
+ "radio",
233
+ ];
234
+ // input 태그이면서 텍스트 입력 타입이나 체크박스/라디오인 경우
235
+ if (tagName === "input") {
236
+ const type = element.type;
237
+ return inputTypes.includes(type);
238
+ }
239
+ // textarea, select, 편집 가능한 요소들
240
+ if (["textarea", "select", "button"].includes(tagName)) {
241
+ return true;
242
+ }
243
+ // SVG 요소들 (아이콘들)
244
+ if ([
245
+ "svg",
246
+ "path",
247
+ "circle",
248
+ "rect",
249
+ "line",
250
+ "polygon",
251
+ "polyline",
252
+ ].includes(tagName)) {
253
+ return true;
254
+ }
255
+ // contenteditable 속성이 있는 요소
256
+ if (element.getAttribute("contenteditable") === "true") {
257
+ return true;
258
+ }
259
+ // 추가 셀렉터 체크
260
+ if (config === null || config === void 0 ? void 0 : config.excludeSelectors) {
261
+ for (const selector of config.excludeSelectors) {
262
+ if (element.matches(selector)) {
263
+ return true;
264
+ }
265
+ }
266
+ }
267
+ return checkElementAndParents(element, config);
268
+ };
269
+ /**
270
+ * 자신 또는 부모 요소들을 확인하여 드래그 스크롤을 제외할 요소인지 판단
271
+ */
272
+ const checkElementAndParents = (element, config) => {
273
+ // 모든 제외 클래스들 합치기 (기본 클래스 + 사용자 추가 클래스)
274
+ const allExcludeClasses = [
275
+ ...DEFAULT_EXCLUDE_CLASSES,
276
+ ...((config === null || config === void 0 ? void 0 : config.excludeClasses) || []),
277
+ ];
278
+ let currentElement = element;
279
+ let depth = 0;
280
+ const maxDepth = 5; // 최대 5단계까지 부모 요소 확인
281
+ while (currentElement && depth <= maxDepth) {
282
+ // 현재 요소가 제외 클래스를 가지고 있는지 확인
283
+ if (allExcludeClasses.some((cls) => currentElement.classList.contains(cls))) {
284
+ return true;
285
+ }
286
+ // 다이얼로그 루트에 도달하면 중단
287
+ if (currentElement.classList.contains("MuiDialogContent-root")) {
288
+ break;
289
+ }
290
+ currentElement = currentElement.parentElement;
291
+ depth++;
292
+ }
293
+ return false;
294
+ };
295
+
296
+ const OverlayScrollbar = react.forwardRef(({ className = "", style = {}, children, onScroll, scrollContainer: externalScrollContainer,
297
+ // 그룹화된 설정 객체들
298
+ thumb = {}, track = {}, arrows = {}, dragScroll = {}, autoHide = {},
299
+ // 기타 설정들
300
+ showScrollbar = true, }, ref) => {
10
301
  const containerRef = react.useRef(null);
11
302
  const contentRef = react.useRef(null);
12
303
  const scrollbarRef = react.useRef(null);
@@ -14,9 +305,18 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
14
305
  // 기본 상태들
15
306
  const [scrollbarVisible, setScrollbarVisible] = react.useState(false);
16
307
  const [isDragging, setIsDragging] = react.useState(false);
308
+ const [isThumbHovered, setIsThumbHovered] = react.useState(false);
17
309
  const [dragStart, setDragStart] = react.useState({ y: 0, scrollTop: 0 });
18
310
  const [thumbHeight, setThumbHeight] = react.useState(0);
19
311
  const [thumbTop, setThumbTop] = react.useState(0);
312
+ // 드래그 스크롤 상태
313
+ const [isDragScrolling, setIsDragScrolling] = react.useState(false);
314
+ const [dragScrollStart, setDragScrollStart] = react.useState({
315
+ x: 0,
316
+ y: 0,
317
+ scrollTop: 0,
318
+ scrollLeft: 0,
319
+ });
20
320
  const [activeArrow, setActiveArrow] = react.useState(null);
21
321
  const [hoveredArrow, setHoveredArrow] = react.useState(null);
22
322
  // 초기 마운트 시 hover 방지용
@@ -26,6 +326,65 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
26
326
  const [isWheelScrolling, setIsWheelScrolling] = react.useState(false);
27
327
  // 숨김 타이머
28
328
  const hideTimeoutRef = react.useRef(null);
329
+ // 그룹화된 설정 객체들에 기본값 설정
330
+ const finalThumbConfig = react.useMemo(() => {
331
+ var _a, _b, _c, _d, _e, _f, _g, _h;
332
+ const baseColor = (_a = thumb.color) !== null && _a !== void 0 ? _a : "#606060";
333
+ return {
334
+ width: (_b = thumb.width) !== null && _b !== void 0 ? _b : 8,
335
+ minHeight: (_c = thumb.minHeight) !== null && _c !== void 0 ? _c : 50,
336
+ radius: (_d = thumb.radius) !== null && _d !== void 0 ? _d : ((_e = thumb.width) !== null && _e !== void 0 ? _e : 8) / 2,
337
+ color: baseColor,
338
+ opacity: (_f = thumb.opacity) !== null && _f !== void 0 ? _f : 0.6,
339
+ hoverColor: (_g = thumb.hoverColor) !== null && _g !== void 0 ? _g : baseColor,
340
+ hoverOpacity: (_h = thumb.hoverOpacity) !== null && _h !== void 0 ? _h : 1.0,
341
+ };
342
+ }, [thumb]);
343
+ const finalTrackConfig = react.useMemo(() => {
344
+ var _a, _b, _c, _d, _e, _f, _g;
345
+ return ({
346
+ width: (_a = track.width) !== null && _a !== void 0 ? _a : 16,
347
+ color: (_b = track.color) !== null && _b !== void 0 ? _b : "rgba(128, 128, 128, 0.1)",
348
+ visible: (_c = track.visible) !== null && _c !== void 0 ? _c : true,
349
+ alignment: (_d = track.alignment) !== null && _d !== void 0 ? _d : "center",
350
+ radius: (_f = (_e = track.radius) !== null && _e !== void 0 ? _e : finalThumbConfig.radius) !== null && _f !== void 0 ? _f : 4,
351
+ margin: (_g = track.margin) !== null && _g !== void 0 ? _g : 4,
352
+ });
353
+ }, [track, finalThumbConfig.radius]);
354
+ const finalArrowsConfig = react.useMemo(() => {
355
+ var _a, _b, _c, _d, _e, _f;
356
+ const baseColor = (_a = arrows.color) !== null && _a !== void 0 ? _a : "#808080";
357
+ return {
358
+ visible: (_b = arrows.visible) !== null && _b !== void 0 ? _b : false,
359
+ step: (_c = arrows.step) !== null && _c !== void 0 ? _c : 50,
360
+ color: baseColor,
361
+ opacity: (_d = arrows.opacity) !== null && _d !== void 0 ? _d : 0.6,
362
+ hoverColor: (_e = arrows.hoverColor) !== null && _e !== void 0 ? _e : baseColor,
363
+ hoverOpacity: (_f = arrows.hoverOpacity) !== null && _f !== void 0 ? _f : 1.0,
364
+ };
365
+ }, [arrows]);
366
+ const finalDragScrollConfig = react.useMemo(() => {
367
+ var _a, _b, _c;
368
+ return ({
369
+ enabled: (_a = dragScroll.enabled) !== null && _a !== void 0 ? _a : true,
370
+ excludeClasses: (_b = dragScroll.excludeClasses) !== null && _b !== void 0 ? _b : [],
371
+ excludeSelectors: (_c = dragScroll.excludeSelectors) !== null && _c !== void 0 ? _c : [],
372
+ });
373
+ }, [dragScroll]);
374
+ const finalAutoHideConfig = react.useMemo(() => {
375
+ var _a, _b, _c;
376
+ return ({
377
+ enabled: (_a = autoHide.enabled) !== null && _a !== void 0 ? _a : true,
378
+ delay: (_b = autoHide.delay) !== null && _b !== void 0 ? _b : 1500,
379
+ delayOnWheel: (_c = autoHide.delayOnWheel) !== null && _c !== void 0 ? _c : 700,
380
+ });
381
+ }, [autoHide]);
382
+ // 호환성을 위한 변수들 (자주 사용되는 변수들만 유지)
383
+ const finalThumbWidth = finalThumbConfig.width;
384
+ const finalTrackWidth = finalTrackConfig.width;
385
+ const thumbMinHeight = finalThumbConfig.minHeight;
386
+ const showArrows = finalArrowsConfig.visible;
387
+ const arrowStep = finalArrowsConfig.step;
29
388
  // ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
30
389
  react.useImperativeHandle(ref, () => ({
31
390
  getScrollContainer: () => containerRef.current,
@@ -47,13 +406,61 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
47
406
  return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
48
407
  },
49
408
  }), []);
409
+ // 실제 스크롤 가능한 요소 찾기
410
+ const findScrollableElement = react.useCallback(() => {
411
+ // externalScrollContainer가 있으면 우선 사용
412
+ if (externalScrollContainer) {
413
+ console.log("OverlayScrollbar: checking external container", {
414
+ scrollHeight: externalScrollContainer.scrollHeight,
415
+ clientHeight: externalScrollContainer.clientHeight,
416
+ });
417
+ // virtuoso의 내부 스크롤러를 찾기
418
+ const virtuosoScroller = externalScrollContainer.querySelector('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller');
419
+ if (virtuosoScroller) {
420
+ const element = virtuosoScroller;
421
+ if (element.scrollHeight > element.clientHeight + 2) {
422
+ console.log("OverlayScrollbar: found virtuoso scroller", {
423
+ scrollHeight: element.scrollHeight,
424
+ clientHeight: element.clientHeight,
425
+ element,
426
+ });
427
+ return element;
428
+ }
429
+ }
430
+ // externalScrollContainer 자체가 스크롤 가능한지 확인
431
+ if (externalScrollContainer.scrollHeight >
432
+ externalScrollContainer.clientHeight + 2) {
433
+ return externalScrollContainer;
434
+ }
435
+ }
436
+ if (!containerRef.current) {
437
+ return null;
438
+ }
439
+ // 내부 컨테이너의 스크롤 가능 여부 확인
440
+ if (contentRef.current &&
441
+ contentRef.current.scrollHeight >
442
+ containerRef.current.clientHeight + 2) {
443
+ return containerRef.current;
444
+ }
445
+ // children 요소에서 스크롤 가능한 요소 찾기
446
+ const childScrollableElements = containerRef.current.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
447
+ for (const child of childScrollableElements) {
448
+ const element = child;
449
+ if (element.scrollHeight > element.clientHeight + 2) {
450
+ console.log("OverlayScrollbar: found scrollable child element", {
451
+ scrollHeight: element.scrollHeight,
452
+ clientHeight: element.clientHeight,
453
+ element,
454
+ });
455
+ return element;
456
+ }
457
+ }
458
+ return null;
459
+ }, [externalScrollContainer]);
50
460
  // 스크롤 가능 여부 체크
51
461
  const isScrollable = react.useCallback(() => {
52
- if (!containerRef.current || !contentRef.current)
53
- return false;
54
- return (contentRef.current.scrollHeight >
55
- containerRef.current.clientHeight + 2);
56
- }, []);
462
+ return findScrollableElement() !== null;
463
+ }, [findScrollableElement]);
57
464
  // 타이머 정리
58
465
  const clearHideTimer = react.useCallback(() => {
59
466
  if (hideTimeoutRef.current) {
@@ -63,31 +470,45 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
63
470
  }, []);
64
471
  // 스크롤바 숨기기 타이머
65
472
  const setHideTimer = react.useCallback((delay) => {
473
+ // 자동 숨김이 비활성화되어 있으면 타이머를 설정하지 않음
474
+ if (!finalAutoHideConfig.enabled) {
475
+ return;
476
+ }
66
477
  clearHideTimer();
67
478
  hideTimeoutRef.current = setTimeout(() => {
68
479
  setScrollbarVisible(false);
69
480
  hideTimeoutRef.current = null;
70
481
  }, delay);
71
- }, [clearHideTimer, isDragging]);
482
+ }, [clearHideTimer, finalAutoHideConfig.enabled]);
72
483
  // 스크롤바 위치 및 크기 업데이트
73
484
  const updateScrollbar = react.useCallback(() => {
74
- if (!containerRef.current ||
75
- !contentRef.current ||
76
- !scrollbarRef.current)
485
+ if (!scrollbarRef.current)
77
486
  return;
78
- const container = containerRef.current;
79
- const content = contentRef.current;
80
- const containerHeight = container.clientHeight;
81
- const contentHeight = content.scrollHeight;
82
- const scrollTop = container.scrollTop;
83
- // 스크롤 불가능하면 숨김
84
- if (contentHeight <= containerHeight + 2) {
487
+ const scrollableElement = findScrollableElement();
488
+ if (!scrollableElement) {
489
+ // 스크롤 불가능하면 숨김
85
490
  setScrollbarVisible(false);
86
491
  clearHideTimer();
87
492
  return;
88
493
  }
89
- // 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
90
- const arrowSpace = showArrows ? scrollbarWidth * 2 + 16 : 8;
494
+ // 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
495
+ if (!finalAutoHideConfig.enabled) {
496
+ setScrollbarVisible(true);
497
+ clearHideTimer();
498
+ }
499
+ const containerHeight = scrollableElement.clientHeight;
500
+ const contentHeight = scrollableElement.scrollHeight;
501
+ const scrollTop = scrollableElement.scrollTop;
502
+ console.log("OverlayScrollbar: updating scrollbar", {
503
+ containerHeight,
504
+ contentHeight,
505
+ scrollTop,
506
+ element: scrollableElement,
507
+ });
508
+ // 화살표와 간격 공간 계산 (화살표 + 위아래 마진, 화살표 없어도 위아래 마진)
509
+ const arrowSpace = showArrows
510
+ ? finalThumbWidth * 2 + finalTrackConfig.margin * 4
511
+ : finalTrackConfig.margin * 2;
91
512
  // 썸 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
92
513
  const availableHeight = containerHeight - arrowSpace;
93
514
  const scrollRatio = containerHeight / contentHeight;
@@ -100,66 +521,110 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
100
521
  : 0;
101
522
  setThumbHeight(calculatedThumbHeight);
102
523
  setThumbTop(calculatedThumbTop);
103
- }, [clearHideTimer, showArrows, scrollbarWidth, thumbMinHeight]);
524
+ }, [
525
+ findScrollableElement,
526
+ clearHideTimer,
527
+ showArrows,
528
+ finalThumbWidth,
529
+ thumbMinHeight,
530
+ finalAutoHideConfig.enabled,
531
+ ]);
104
532
  // 썸 드래그 시작
105
533
  const handleThumbMouseDown = react.useCallback((event) => {
534
+ var _a;
106
535
  event.preventDefault();
107
536
  event.stopPropagation();
108
- if (!containerRef.current)
537
+ const actualScrollContainer = findScrollableElement();
538
+ if (!actualScrollContainer) {
539
+ console.log("Thumb drag - no scrollable element found");
109
540
  return;
541
+ }
110
542
  setIsDragging(true);
111
543
  setDragStart({
112
544
  y: event.clientY,
113
- scrollTop: containerRef.current.scrollTop,
545
+ scrollTop: actualScrollContainer.scrollTop,
114
546
  });
115
547
  clearHideTimer();
116
548
  setScrollbarVisible(true);
117
- }, [clearHideTimer]);
549
+ // 포커스 유지 (키보드 입력이 계속 작동하도록)
550
+ (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
551
+ }, [findScrollableElement, clearHideTimer]);
118
552
  // 썸 드래그 중
119
553
  const handleMouseMove = react.useCallback((event) => {
120
- if (!isDragging || !containerRef.current || !contentRef.current)
554
+ if (!isDragging)
121
555
  return;
122
- const container = containerRef.current;
123
- const content = contentRef.current;
124
- const containerHeight = container.clientHeight;
125
- const contentHeight = content.scrollHeight;
556
+ const actualScrollContainer = findScrollableElement();
557
+ if (!actualScrollContainer) {
558
+ console.log("Mouse move - no scrollable element found");
559
+ return;
560
+ }
561
+ const containerHeight = actualScrollContainer.clientHeight;
562
+ const contentHeight = actualScrollContainer.scrollHeight;
126
563
  const scrollableHeight = contentHeight - containerHeight;
127
564
  const deltaY = event.clientY - dragStart.y;
128
565
  const thumbScrollableHeight = containerHeight - thumbHeight;
129
566
  const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
130
567
  const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
131
- container.scrollTop = newScrollTop;
568
+ actualScrollContainer.scrollTop = newScrollTop;
132
569
  updateScrollbar();
133
- }, [isDragging, dragStart, thumbHeight, updateScrollbar]);
570
+ }, [
571
+ isDragging,
572
+ dragStart,
573
+ thumbHeight,
574
+ updateScrollbar,
575
+ findScrollableElement,
576
+ ]);
134
577
  // 썸 드래그 종료
135
578
  const handleMouseUp = react.useCallback(() => {
136
579
  setIsDragging(false);
137
580
  if (isScrollable()) {
138
- setHideTimer(hideDelay); // 기본 숨김 시간 적용
581
+ setHideTimer(finalAutoHideConfig.delay); // 기본 숨김 시간 적용
139
582
  }
140
- }, [isScrollable, setHideTimer, hideDelay]);
583
+ }, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
141
584
  // 트랙 클릭으로 스크롤 점프
142
585
  const handleTrackClick = react.useCallback((event) => {
143
- if (!containerRef.current ||
144
- !contentRef.current ||
145
- !scrollbarRef.current)
586
+ var _a;
587
+ console.log("handleTrackClick called", event);
588
+ if (!scrollbarRef.current) {
589
+ console.log("Track click - scrollbarRef not available");
146
590
  return;
591
+ }
147
592
  const scrollbar = scrollbarRef.current;
148
593
  const rect = scrollbar.getBoundingClientRect();
149
594
  const clickY = event.clientY - rect.top;
150
- const container = containerRef.current;
151
- const content = contentRef.current;
152
- const containerHeight = container.clientHeight;
153
- const contentHeight = content.scrollHeight;
595
+ const actualScrollContainer = findScrollableElement();
596
+ if (!actualScrollContainer) {
597
+ console.log("Track click - no scrollable element found");
598
+ return;
599
+ }
600
+ console.log("Track click - using scrollable element", actualScrollContainer);
601
+ const containerHeight = actualScrollContainer.clientHeight;
602
+ const contentHeight = actualScrollContainer.scrollHeight;
154
603
  const scrollRatio = clickY / containerHeight;
155
604
  const newScrollTop = scrollRatio * (contentHeight - containerHeight);
156
- container.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
605
+ console.log("Track click scroll calculation", {
606
+ clickY,
607
+ containerHeight,
608
+ contentHeight,
609
+ scrollRatio,
610
+ newScrollTop,
611
+ actualScrollContainer,
612
+ });
613
+ actualScrollContainer.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
157
614
  updateScrollbar();
158
615
  setScrollbarVisible(true);
159
- setHideTimer(hideDelay);
160
- }, [updateScrollbar, setHideTimer, hideDelay]);
616
+ setHideTimer(finalAutoHideConfig.delay);
617
+ // 포커스 유지 (키보드 입력이 계속 작동하도록)
618
+ (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
619
+ }, [
620
+ updateScrollbar,
621
+ setHideTimer,
622
+ finalAutoHideConfig.delay,
623
+ findScrollableElement,
624
+ ]);
161
625
  // 위쪽 화살표 클릭 핸들러
162
626
  const handleUpArrowClick = react.useCallback((event) => {
627
+ var _a;
163
628
  event.preventDefault();
164
629
  event.stopPropagation();
165
630
  if (!containerRef.current)
@@ -168,10 +633,18 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
168
633
  containerRef.current.scrollTop = newScrollTop;
169
634
  updateScrollbar();
170
635
  setScrollbarVisible(true);
171
- setHideTimer(hideDelay);
172
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
636
+ setHideTimer(finalAutoHideConfig.delay);
637
+ // 포커스 유지 (키보드 입력이 계속 작동하도록)
638
+ (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
639
+ }, [
640
+ updateScrollbar,
641
+ setHideTimer,
642
+ arrowStep,
643
+ finalAutoHideConfig.delay,
644
+ ]);
173
645
  // 아래쪽 화살표 클릭 핸들러
174
646
  const handleDownArrowClick = react.useCallback((event) => {
647
+ var _a;
175
648
  event.preventDefault();
176
649
  event.stopPropagation();
177
650
  if (!containerRef.current || !contentRef.current)
@@ -183,20 +656,90 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
183
656
  container.scrollTop = newScrollTop;
184
657
  updateScrollbar();
185
658
  setScrollbarVisible(true);
186
- setHideTimer(hideDelay);
187
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
188
- // 스크롤 이벤트 리스너
189
- react.useEffect(() => {
190
- const container = containerRef.current;
191
- if (!container)
659
+ setHideTimer(finalAutoHideConfig.delay);
660
+ // 포커스 유지 (키보드 입력이 계속 작동하도록)
661
+ (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
662
+ }, [
663
+ updateScrollbar,
664
+ setHideTimer,
665
+ arrowStep,
666
+ finalAutoHideConfig.delay,
667
+ ]);
668
+ // 드래그 스크롤 시작
669
+ const handleDragScrollStart = react.useCallback((event) => {
670
+ // 드래그 스크롤이 비활성화된 경우
671
+ if (!finalDragScrollConfig.enabled)
672
+ return;
673
+ // 텍스트 입력 요소나 제외 대상이면 드래그 스크롤 하지 않음
674
+ const target = event.target;
675
+ if (isTextInputElement(target, finalDragScrollConfig)) {
676
+ return;
677
+ }
678
+ // 오른쪽 클릭이나 휠 클릭은 제외
679
+ if (event.button !== 0)
680
+ return;
681
+ const scrollableElement = findScrollableElement();
682
+ if (!scrollableElement)
192
683
  return;
684
+ // 스크롤 가능한 영역이 아니면 제외
685
+ if (scrollableElement.scrollHeight <=
686
+ scrollableElement.clientHeight)
687
+ return;
688
+ event.preventDefault();
689
+ setIsDragScrolling(true);
690
+ setDragScrollStart({
691
+ x: event.clientX,
692
+ y: event.clientY,
693
+ scrollTop: scrollableElement.scrollTop,
694
+ scrollLeft: scrollableElement.scrollLeft || 0,
695
+ });
696
+ // 스크롤바 표시
697
+ clearHideTimer();
698
+ setScrollbarVisible(true);
699
+ }, [
700
+ finalDragScrollConfig,
701
+ isTextInputElement,
702
+ findScrollableElement,
703
+ clearHideTimer,
704
+ ]);
705
+ // 드래그 스크롤 중
706
+ const handleDragScrollMove = react.useCallback((event) => {
707
+ if (!isDragScrolling)
708
+ return;
709
+ const scrollableElement = findScrollableElement();
710
+ if (!scrollableElement)
711
+ return;
712
+ dragScrollStart.x - event.clientX;
713
+ const deltaY = dragScrollStart.y - event.clientY;
714
+ // 세로 스크롤만 처리 (가로 스크롤은 필요시 나중에 추가)
715
+ const newScrollTop = Math.max(0, Math.min(scrollableElement.scrollHeight -
716
+ scrollableElement.clientHeight, dragScrollStart.scrollTop + deltaY));
717
+ scrollableElement.scrollTop = newScrollTop;
718
+ updateScrollbar();
719
+ }, [
720
+ isDragScrolling,
721
+ dragScrollStart,
722
+ findScrollableElement,
723
+ updateScrollbar,
724
+ ]);
725
+ // 드래그 스크롤 종료
726
+ const handleDragScrollEnd = react.useCallback(() => {
727
+ setIsDragScrolling(false);
728
+ if (isScrollable()) {
729
+ setHideTimer(finalAutoHideConfig.delay);
730
+ }
731
+ }, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
732
+ // 스크롤 이벤트 리스너 (externalScrollContainer 우선 사용)
733
+ react.useEffect(() => {
193
734
  const handleScroll = (event) => {
194
735
  updateScrollbar();
195
736
  // 스크롤 중에는 스크롤바 표시
196
737
  clearHideTimer();
197
738
  setScrollbarVisible(true);
198
739
  // 휠 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
199
- const delay = isWheelScrolling ? hideDelayOnWheel : hideDelay;
740
+ const delay = isWheelScrolling
741
+ ? finalAutoHideConfig.delayOnWheel
742
+ : finalAutoHideConfig.delay;
200
743
  setHideTimer(delay);
201
744
  if (onScroll) {
202
745
  onScroll(event);
@@ -216,28 +759,62 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
216
759
  clearHideTimer();
217
760
  setScrollbarVisible(true);
218
761
  };
219
- container.addEventListener("scroll", handleScroll, {
220
- passive: true,
221
- });
222
- container.addEventListener("wheel", handleWheel, {
223
- passive: true,
762
+ const elementsToWatch = [];
763
+ // 실제 스크롤 가능한 요소 찾기
764
+ const scrollableElement = findScrollableElement();
765
+ if (scrollableElement) {
766
+ elementsToWatch.push(scrollableElement);
767
+ console.log("OverlayScrollbar: watching scrollable element for events", scrollableElement);
768
+ }
769
+ // fallback: 내부 컨테이너와 children 요소도 감지
770
+ const container = containerRef.current;
771
+ if (container && !scrollableElement) {
772
+ elementsToWatch.push(container);
773
+ // children 요소들의 스크롤도 감지
774
+ const childScrollableElements = container.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
775
+ childScrollableElements.forEach((child) => {
776
+ elementsToWatch.push(child);
777
+ });
778
+ }
779
+ // 모든 요소에 이벤트 리스너 등록
780
+ elementsToWatch.forEach((element) => {
781
+ element.addEventListener("scroll", handleScroll, {
782
+ passive: true,
783
+ });
784
+ element.addEventListener("wheel", handleWheel, {
785
+ passive: true,
786
+ });
224
787
  });
225
788
  return () => {
226
- container.removeEventListener("scroll", handleScroll);
227
- container.removeEventListener("wheel", handleWheel);
789
+ // 모든 이벤트 리스너 제거
790
+ elementsToWatch.forEach((element) => {
791
+ element.removeEventListener("scroll", handleScroll);
792
+ element.removeEventListener("wheel", handleWheel);
793
+ });
228
794
  if (wheelTimeoutRef.current) {
229
795
  clearTimeout(wheelTimeoutRef.current);
230
796
  }
231
797
  };
232
798
  }, [
799
+ findScrollableElement,
233
800
  updateScrollbar,
234
801
  onScroll,
235
802
  clearHideTimer,
236
803
  setHideTimer,
237
- hideDelay,
238
- hideDelayOnWheel,
804
+ finalAutoHideConfig,
239
805
  isWheelScrolling,
240
806
  ]);
807
+ // 드래그 스크롤 전역 마우스 이벤트 리스너
808
+ react.useEffect(() => {
809
+ if (isDragScrolling) {
810
+ document.addEventListener("mousemove", handleDragScrollMove);
811
+ document.addEventListener("mouseup", handleDragScrollEnd);
812
+ return () => {
813
+ document.removeEventListener("mousemove", handleDragScrollMove);
814
+ document.removeEventListener("mouseup", handleDragScrollEnd);
815
+ };
816
+ }
817
+ }, [isDragScrolling, handleDragScrollMove, handleDragScrollEnd]);
241
818
  // 전역 마우스 이벤트 리스너
242
819
  react.useEffect(() => {
243
820
  if (isDragging) {
@@ -251,44 +828,68 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
251
828
  }, [isDragging, handleMouseMove, handleMouseUp]);
252
829
  // 초기 스크롤바 업데이트
253
830
  react.useEffect(() => {
831
+ // 즉시 업데이트
254
832
  updateScrollbar();
833
+ // 약간의 지연 후에도 업데이트 (DOM이 완전히 렌더링된 후)
834
+ const timer = setTimeout(() => {
835
+ updateScrollbar();
836
+ }, 100);
837
+ return () => clearTimeout(timer);
255
838
  }, [updateScrollbar]);
839
+ // externalScrollContainer가 변경될 때 스크롤바 업데이트
840
+ react.useEffect(() => {
841
+ if (externalScrollContainer) {
842
+ // externalScrollContainer가 설정된 후 스크롤바 업데이트
843
+ const timer = setTimeout(() => {
844
+ updateScrollbar();
845
+ }, 50);
846
+ return () => clearTimeout(timer);
847
+ }
848
+ }, [externalScrollContainer, updateScrollbar]);
256
849
  // 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
257
850
  react.useEffect(() => {
258
851
  const timer = setTimeout(() => {
259
852
  setIsInitialized(true);
260
- }, 100); // 100ms 후 초기화 완료
853
+ console.log("OverlayScrollbar initialized", {
854
+ containerRef: !!containerRef.current,
855
+ contentRef: !!contentRef.current,
856
+ isScrollable: isScrollable(),
857
+ autoHideEnabled: finalAutoHideConfig.enabled,
858
+ });
859
+ // 초기화 후 스크롤바 업데이트 (썸 높이 정확하게 계산)
860
+ updateScrollbar();
861
+ // 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
862
+ if (!finalAutoHideConfig.enabled && isScrollable()) {
863
+ setScrollbarVisible(true);
864
+ }
865
+ }, 100);
261
866
  return () => clearTimeout(timer);
262
- }, []);
867
+ }, [isScrollable, updateScrollbar, finalAutoHideConfig.enabled]);
263
868
  // Resize observer로 크기 변경 감지
264
869
  react.useEffect(() => {
265
- if (!containerRef.current || !contentRef.current)
266
- return;
267
870
  const resizeObserver = new ResizeObserver(() => {
268
871
  updateScrollbar();
269
872
  });
270
- resizeObserver.observe(containerRef.current);
271
- resizeObserver.observe(contentRef.current);
272
- return () => resizeObserver.disconnect();
273
- }, [updateScrollbar]);
274
- // 계산된 값들을 메모이제이션하여 안정화
275
- const { finalThumbWidth, finalTrackWidth } = react.useMemo(() => {
276
- const computedThumbWidth = thumbWidth !== undefined ? thumbWidth : scrollbarWidth;
277
- let computedTrackWidth = trackWidth !== undefined ? trackWidth : scrollbarWidth * 2;
278
- // thumbWidth가 trackWidth보다 크거나 같으면 trackWidth를 thumbWidth와 같게 설정
279
- if (computedThumbWidth >= computedTrackWidth) {
280
- computedTrackWidth = computedThumbWidth;
873
+ const elementsToObserve = [];
874
+ // externalScrollContainer가 있으면 우선 관찰
875
+ if (externalScrollContainer) {
876
+ elementsToObserve.push(externalScrollContainer);
281
877
  }
282
- return {
283
- finalThumbWidth: computedThumbWidth,
284
- finalTrackWidth: computedTrackWidth,
285
- };
286
- }, [thumbWidth, trackWidth, scrollbarWidth]);
287
- // 썸 radius 계산 (기본값: thumbWidth / 2)
288
- const calculatedThumbRadius = thumbRadius !== undefined ? thumbRadius : finalThumbWidth / 2;
289
- // 화살표 색상 계산 (기본값: 독립적인 색상)
290
- const finalArrowColor = arrowColor || "rgba(128, 128, 128, 0.8)";
291
- const finalArrowActiveColor = arrowActiveColor || "rgba(128, 128, 128, 1.0)";
878
+ // 내부 컨테이너들도 관찰
879
+ if (containerRef.current) {
880
+ elementsToObserve.push(containerRef.current);
881
+ }
882
+ if (contentRef.current) {
883
+ elementsToObserve.push(contentRef.current);
884
+ }
885
+ // 모든 요소들 관찰 시작
886
+ elementsToObserve.forEach((element) => {
887
+ resizeObserver.observe(element);
888
+ });
889
+ return () => resizeObserver.disconnect();
890
+ }, [updateScrollbar, externalScrollContainer]);
891
+ // trackWidth가 thumbWidth보다 작으면 thumbWidth와 같게 설정
892
+ const adjustedTrackWidth = Math.max(finalTrackWidth, finalThumbWidth);
292
893
  // 웹킷 스크롤바 숨기기용 CSS 동적 주입
293
894
  react.useEffect(() => {
294
895
  const styleId = "overlay-scrollbar-webkit-hide";
@@ -311,6 +912,14 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
311
912
  .overlay-scrollbar-container::-webkit-scrollbar-thumb {
312
913
  display: none !important;
313
914
  }
915
+ .overlay-scrollbar-container:focus {
916
+ outline: 2px solid rgba(0, 123, 255, 0.3);
917
+ outline-offset: -2px;
918
+ }
919
+ .overlay-scrollbar-container:focus-visible {
920
+ outline: 2px solid rgba(0, 123, 255, 0.5);
921
+ outline-offset: -2px;
922
+ }
314
923
  `;
315
924
  document.head.appendChild(style);
316
925
  return () => {
@@ -320,68 +929,100 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
320
929
  }
321
930
  };
322
931
  }, []);
323
- return (jsxRuntime.jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsxRuntime.jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
932
+ return (jsxRuntime.jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsxRuntime.jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", tabIndex: -1, onMouseDown: handleDragScrollStart, style: {
324
933
  width: "100%", // 명시적 너비 설정
325
- height: "100%", // 상위 컨테이너의 전체 높이 사용
934
+ height: "100%", // 부모의 전체 높이 사용
935
+ flex: "1 1 auto", // flex item으로 설정하여 높이를 자동으로 계산
326
936
  minHeight: 0, // 최소 높이 보장
327
937
  overflow: "auto", // 네이티브 스크롤 기능 유지
328
938
  // 브라우저 기본 스크롤바만 숨기기
329
939
  scrollbarWidth: "none", // Firefox
330
940
  msOverflowStyle: "none", // IE/Edge
941
+ // 키보드 포커스 스타일 (접근성)
942
+ outline: "none", // 기본 아웃라인 제거
943
+ userSelect: isDragScrolling ? "none" : "auto", // 드래그 중 텍스트 선택 방지
331
944
  }, children: jsxRuntime.jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: {
332
- minHeight: "100%",
945
+ height: "100%", // min-height 대신 height 사용
946
+ minHeight: 0, // flex shrink 허용
947
+ display: "flex", // flex 컨테이너로 설정
948
+ flexDirection: "column", // 세로 방향 정렬
333
949
  }, children: children }) }), showScrollbar && (jsxRuntime.jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
334
- if (isScrollable()) {
335
- clearHideTimer();
336
- setScrollbarVisible(true);
337
- }
950
+ console.log("Track hover enter", {
951
+ isScrollable: isScrollable(),
952
+ scrollbarVisible,
953
+ });
954
+ clearHideTimer();
955
+ setScrollbarVisible(true);
338
956
  }, onMouseLeave: () => {
339
- if (!isDragging && isScrollable()) {
340
- setHideTimer(hideDelay);
957
+ console.log("Track hover leave", { isDragging });
958
+ if (!isDragging) {
959
+ setHideTimer(finalAutoHideConfig.delay);
341
960
  }
342
961
  }, style: {
343
962
  position: "absolute",
344
963
  top: 0,
345
- right: 0, // 완전히 오른쪽에 붙임
346
- width: `${finalTrackWidth}px`, // hover 영역 너비
964
+ right: 0,
965
+ width: `${adjustedTrackWidth}px`,
347
966
  height: "100%",
348
967
  opacity: scrollbarVisible ? 1 : 0,
349
968
  transition: "opacity 0.2s ease-in-out",
350
969
  cursor: "pointer",
351
970
  zIndex: 1000,
352
- pointerEvents: "auto", // 항상 이벤트 활성화 (hover 감지용)
353
- }, children: [jsxRuntime.jsx("div", { className: "overlay-scrollbar-track-background", onClick: handleTrackClick, style: {
971
+ pointerEvents: "auto",
972
+ }, children: [finalTrackConfig.visible && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-track-background", onClick: (e) => {
973
+ console.log("Track background clicked", e);
974
+ e.preventDefault();
975
+ e.stopPropagation();
976
+ handleTrackClick(e);
977
+ }, style: {
354
978
  position: "absolute",
355
979
  top: showArrows
356
- ? `${finalThumbWidth + 8}px`
357
- : "4px",
358
- right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
359
- width: `${finalThumbWidth}px`,
980
+ ? `${finalThumbConfig.width +
981
+ finalTrackConfig.margin * 2}px`
982
+ : `${finalTrackConfig.margin}px`,
983
+ right: finalTrackConfig.alignment === "right"
984
+ ? "0px"
985
+ : `${(adjustedTrackWidth -
986
+ finalThumbConfig.width) /
987
+ 2}px`, // 트랙 정렬
988
+ width: `${finalThumbConfig.width}px`,
360
989
  height: showArrows
361
- ? `calc(100% - ${finalThumbWidth * 2 + 16}px)`
362
- : "calc(100% - 8px)",
363
- backgroundColor: trackColor,
364
- borderRadius: `${calculatedThumbRadius}px`,
990
+ ? `calc(100% - ${finalThumbConfig.width * 2 +
991
+ finalTrackConfig.margin * 4}px)`
992
+ : `calc(100% - ${finalTrackConfig.margin * 2}px)`,
993
+ backgroundColor: finalTrackConfig.color,
994
+ borderRadius: `${finalTrackConfig.radius}px`,
365
995
  cursor: "pointer",
366
- } }), jsxRuntime.jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
996
+ } })), jsxRuntime.jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, onMouseEnter: () => setIsThumbHovered(true), onMouseLeave: () => setIsThumbHovered(false), style: {
367
997
  position: "absolute",
368
- top: `${(showArrows ? finalThumbWidth + 8 : 4) +
369
- thumbTop}px`,
370
- right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
998
+ top: `${(showArrows
999
+ ? finalThumbWidth +
1000
+ finalTrackConfig.margin * 2
1001
+ : finalTrackConfig.margin) + thumbTop}px`,
1002
+ right: finalTrackConfig.alignment === "right"
1003
+ ? "0px"
1004
+ : `${(adjustedTrackWidth -
1005
+ finalThumbWidth) /
1006
+ 2}px`, // 트랙 정렬
371
1007
  width: `${finalThumbWidth}px`,
372
1008
  height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
373
- backgroundColor: isDragging
374
- ? thumbActiveColor
375
- : thumbColor,
376
- borderRadius: `${calculatedThumbRadius}px`,
1009
+ backgroundColor: isThumbHovered || isDragging
1010
+ ? finalThumbConfig.hoverColor
1011
+ : finalThumbConfig.color,
1012
+ opacity: isThumbHovered || isDragging
1013
+ ? finalThumbConfig.hoverOpacity
1014
+ : finalThumbConfig.opacity,
1015
+ borderRadius: `${finalThumbConfig.radius}px`,
377
1016
  cursor: "pointer",
378
- transition: isDragging
379
- ? "none"
380
- : "background-color 0.2s ease-in-out",
1017
+ transition: "background-color 0.2s ease-in-out, opacity 0.2s ease-in-out",
381
1018
  } })] })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
382
1019
  position: "absolute",
383
- top: "4px",
384
- right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
1020
+ top: `${finalTrackConfig.margin}px`,
1021
+ right: finalTrackConfig.alignment === "right"
1022
+ ? "0px"
1023
+ : `${(adjustedTrackWidth -
1024
+ finalThumbWidth) /
1025
+ 2}px`, // 트랙 정렬
385
1026
  width: `${finalThumbWidth}px`,
386
1027
  height: `${finalThumbWidth}px`,
387
1028
  cursor: "pointer",
@@ -390,16 +1031,24 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
390
1031
  justifyContent: "center",
391
1032
  fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
392
1033
  color: hoveredArrow === "up"
393
- ? finalArrowActiveColor
394
- : finalArrowColor,
1034
+ ? finalArrowsConfig.hoverColor
1035
+ : finalArrowsConfig.color,
395
1036
  userSelect: "none",
396
1037
  zIndex: 1001,
397
- opacity: scrollbarVisible ? 1 : 0,
1038
+ opacity: scrollbarVisible
1039
+ ? hoveredArrow === "up"
1040
+ ? finalArrowsConfig.hoverOpacity
1041
+ : finalArrowsConfig.opacity
1042
+ : 0,
398
1043
  transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
399
1044
  }, children: "\u25B2" })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
400
1045
  position: "absolute",
401
- bottom: "4px",
402
- right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
1046
+ bottom: `${finalTrackConfig.margin}px`,
1047
+ right: finalTrackConfig.alignment === "right"
1048
+ ? "0px"
1049
+ : `${(adjustedTrackWidth -
1050
+ finalThumbWidth) /
1051
+ 2}px`, // 트랙 정렬
403
1052
  width: `${finalThumbWidth}px`,
404
1053
  height: `${finalThumbWidth}px`,
405
1054
  cursor: "pointer",
@@ -408,11 +1057,15 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
408
1057
  justifyContent: "center",
409
1058
  fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
410
1059
  color: hoveredArrow === "down"
411
- ? finalArrowActiveColor
412
- : finalArrowColor,
1060
+ ? finalArrowsConfig.hoverColor
1061
+ : finalArrowsConfig.color,
413
1062
  userSelect: "none",
414
1063
  zIndex: 1001,
415
- opacity: scrollbarVisible ? 1 : 0,
1064
+ opacity: scrollbarVisible
1065
+ ? hoveredArrow === "down"
1066
+ ? finalArrowsConfig.hoverOpacity
1067
+ : finalArrowsConfig.opacity
1068
+ : 0,
416
1069
  transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
417
1070
  }, children: "\u25BC" }))] }));
418
1071
  });