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