@ehfuse/overlay-scrollbar 1.0.0 → 1.2.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,52 +5,26 @@ 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(({ children, onScroll, className, style }, ref) => {
8
+ const OverlayScrollbar = react.forwardRef(({ className = "", style = {}, children, onScroll, scrollbarWidth = 8, // deprecated
9
+ thumbRadius, 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) => {
9
10
  const containerRef = react.useRef(null);
10
11
  const contentRef = react.useRef(null);
11
12
  const scrollbarRef = react.useRef(null);
12
13
  const thumbRef = react.useRef(null);
13
- // 웹킷 스크롤바 숨김을 위한 스타일
14
- react.useEffect(() => {
15
- const style = document.createElement("style");
16
- style.textContent = `
17
- .overlay-scrollbar-container::-webkit-scrollbar {
18
- display: none !important;
19
- width: 0 !important;
20
- height: 0 !important;
21
- background: transparent !important;
22
- }
23
- .overlay-scrollbar-container::-webkit-scrollbar-track {
24
- display: none !important;
25
- }
26
- .overlay-scrollbar-container::-webkit-scrollbar-thumb {
27
- display: none !important;
28
- }
29
- .overlay-scrollbar-container::-webkit-scrollbar-corner {
30
- display: none !important;
31
- }
32
- .overlay-scrollbar-container * {
33
- scrollbar-width: none;
34
- -ms-overflow-style: none;
35
- }
36
- .overlay-scrollbar-container *::-webkit-scrollbar {
37
- display: none !important;
38
- width: 0 !important;
39
- height: 0 !important;
40
- }
41
- `;
42
- document.head.appendChild(style);
43
- return () => {
44
- document.head.removeChild(style);
45
- };
46
- }, []);
14
+ // 기본 상태들
47
15
  const [scrollbarVisible, setScrollbarVisible] = react.useState(false);
48
- const [trackVisible, setTrackVisible] = react.useState(false); // 트랙 표시 상태 추가
49
16
  const [isDragging, setIsDragging] = react.useState(false);
50
17
  const [dragStart, setDragStart] = react.useState({ y: 0, scrollTop: 0 });
51
18
  const [thumbHeight, setThumbHeight] = react.useState(0);
52
19
  const [thumbTop, setThumbTop] = react.useState(0);
53
- // 스크롤바 표시/숨김 타이머
20
+ const [activeArrow, setActiveArrow] = react.useState(null);
21
+ const [hoveredArrow, setHoveredArrow] = react.useState(null);
22
+ // 초기 마운트 시 hover 방지용
23
+ const [isInitialized, setIsInitialized] = react.useState(false);
24
+ // 휠 스크롤 감지용
25
+ const wheelTimeoutRef = react.useRef(null);
26
+ const [isWheelScrolling, setIsWheelScrolling] = react.useState(false);
27
+ // 숨김 타이머
54
28
  const hideTimeoutRef = react.useRef(null);
55
29
  // ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
56
30
  react.useImperativeHandle(ref, () => ({
@@ -73,72 +47,60 @@ const OverlayScrollbar = react.forwardRef(({ children, onScroll, className, styl
73
47
  return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
74
48
  },
75
49
  }), []);
76
- // 스크롤바 숨김 타이머 취소
50
+ // 스크롤 가능 여부 체크
51
+ const isScrollable = react.useCallback(() => {
52
+ if (!containerRef.current || !contentRef.current)
53
+ return false;
54
+ return (contentRef.current.scrollHeight >
55
+ containerRef.current.clientHeight + 2);
56
+ }, []);
57
+ // 타이머 정리
77
58
  const clearHideTimer = react.useCallback(() => {
78
59
  if (hideTimeoutRef.current) {
79
60
  clearTimeout(hideTimeoutRef.current);
80
61
  hideTimeoutRef.current = null;
81
62
  }
82
63
  }, []);
83
- // 스크롤바 숨김 타이머 설정
64
+ // 스크롤바 숨기기 타이머
84
65
  const setHideTimer = react.useCallback((delay) => {
85
- clearHideTimer(); // 기존 타이머 취소
66
+ clearHideTimer();
86
67
  hideTimeoutRef.current = setTimeout(() => {
87
- if (!isDragging) {
88
- setScrollbarVisible(false);
89
- setTrackVisible(false);
90
- }
68
+ setScrollbarVisible(false);
91
69
  hideTimeoutRef.current = null;
92
70
  }, delay);
93
- }, [isDragging, clearHideTimer]);
94
- // 스크롤 가능 여부 체크 함수
95
- const isScrollable = react.useCallback(() => {
96
- if (!containerRef.current || !contentRef.current)
97
- return false;
98
- const container = containerRef.current;
99
- const content = contentRef.current;
100
- return content.scrollHeight > container.clientHeight + 2;
101
- }, []);
102
- // 스크롤바 크기 및 위치 계산
71
+ }, [clearHideTimer, isDragging]);
72
+ // 스크롤바 위치 크기 업데이트
103
73
  const updateScrollbar = react.useCallback(() => {
104
- if (!containerRef.current || !contentRef.current)
74
+ if (!containerRef.current ||
75
+ !contentRef.current ||
76
+ !scrollbarRef.current)
105
77
  return;
106
78
  const container = containerRef.current;
107
79
  const content = contentRef.current;
108
80
  const containerHeight = container.clientHeight;
109
81
  const contentHeight = content.scrollHeight;
110
82
  const scrollTop = container.scrollTop;
111
- // console.log("스크롤바 업데이트:", {
112
- // containerHeight,
113
- // contentHeight,
114
- // scrollTop,
115
- // hasScrollableContent: contentHeight > containerHeight,
116
- // });
117
- // 스크롤 가능한 콘텐츠가 있는지 확인 (여유분 2px 추가로 더 정확한 판단)
83
+ // 스크롤 불가능하면 숨김
118
84
  if (contentHeight <= containerHeight + 2) {
119
- // console.log("스크롤 불가능한 콘텐츠, 스크롤바 숨김");
120
85
  setScrollbarVisible(false);
121
- setTrackVisible(false);
122
- clearHideTimer(); // 타이머도 정리
86
+ clearHideTimer();
123
87
  return;
124
88
  }
125
- // 높이 계산 (최소 20px, 최대 컨테이너의 90%)
126
- const thumbHeightRatio = containerHeight / contentHeight;
127
- const calculatedThumbHeight = Math.max(20, Math.min(containerHeight * 0.9, containerHeight * thumbHeightRatio));
128
- // 위치 계산
129
- const scrollRatio = scrollTop / (contentHeight - containerHeight);
130
- const maxThumbTop = containerHeight - calculatedThumbHeight;
131
- const calculatedThumbTop = scrollRatio * maxThumbTop;
132
- // console.log("썸 계산:", {
133
- // thumbHeightRatio,
134
- // calculatedThumbHeight,
135
- // scrollRatio,
136
- // calculatedThumbTop,
137
- // maxThumbTop,
138
- // });
89
+ // 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
90
+ const arrowSpace = showArrows ? scrollbarWidth * 2 + 16 : 8;
91
+ // 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
92
+ const availableHeight = containerHeight - arrowSpace;
93
+ const scrollRatio = containerHeight / contentHeight;
94
+ const calculatedThumbHeight = Math.max(availableHeight * scrollRatio, thumbMinHeight);
95
+ // 위치 계산 (화살표와 간격 공간 제외)
96
+ const scrollableHeight = contentHeight - containerHeight;
97
+ const thumbScrollableHeight = availableHeight - calculatedThumbHeight;
98
+ const calculatedThumbTop = scrollableHeight > 0
99
+ ? (scrollTop / scrollableHeight) * thumbScrollableHeight
100
+ : 0;
139
101
  setThumbHeight(calculatedThumbHeight);
140
102
  setThumbTop(calculatedThumbTop);
141
- }, []);
103
+ }, [clearHideTimer, showArrows, scrollbarWidth, thumbMinHeight]);
142
104
  // 썸 드래그 시작
143
105
  const handleThumbMouseDown = react.useCallback((event) => {
144
106
  event.preventDefault();
@@ -150,24 +112,21 @@ const OverlayScrollbar = react.forwardRef(({ children, onScroll, className, styl
150
112
  y: event.clientY,
151
113
  scrollTop: containerRef.current.scrollTop,
152
114
  });
115
+ clearHideTimer();
153
116
  setScrollbarVisible(true);
154
- setTrackVisible(true); // 드래그 시 트랙 표시
155
- clearHideTimer(); // 드래그 중에는 타이머 취소
156
117
  }, [clearHideTimer]);
157
118
  // 썸 드래그 중
158
119
  const handleMouseMove = react.useCallback((event) => {
159
120
  if (!isDragging || !containerRef.current || !contentRef.current)
160
121
  return;
161
- event.preventDefault();
162
122
  const container = containerRef.current;
163
123
  const content = contentRef.current;
164
124
  const containerHeight = container.clientHeight;
165
125
  const contentHeight = content.scrollHeight;
166
- const deltaY = event.clientY - dragStart.y;
167
126
  const scrollableHeight = contentHeight - containerHeight;
168
- const maxThumbTop = containerHeight - thumbHeight;
169
- // 드래그 거리를 스크롤 거리로 변환
170
- const scrollDelta = (deltaY / maxThumbTop) * scrollableHeight;
127
+ const deltaY = event.clientY - dragStart.y;
128
+ const thumbScrollableHeight = containerHeight - thumbHeight;
129
+ const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
171
130
  const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
172
131
  container.scrollTop = newScrollTop;
173
132
  updateScrollbar();
@@ -175,10 +134,11 @@ const OverlayScrollbar = react.forwardRef(({ children, onScroll, className, styl
175
134
  // 썸 드래그 종료
176
135
  const handleMouseUp = react.useCallback(() => {
177
136
  setIsDragging(false);
178
- setTrackVisible(false); // 드래그 종료 시 트랙 숨김
179
- setHideTimer(2000); // 2초 숨김
180
- }, [setHideTimer]);
181
- // 스크롤바 트랙 클릭
137
+ if (isScrollable()) {
138
+ setHideTimer(hideDelay); // 기본 숨김 시간 적용
139
+ }
140
+ }, [isScrollable, setHideTimer, hideDelay]);
141
+ // 트랙 클릭으로 스크롤 점프
182
142
  const handleTrackClick = react.useCallback((event) => {
183
143
  if (!containerRef.current ||
184
144
  !contentRef.current ||
@@ -196,43 +156,89 @@ const OverlayScrollbar = react.forwardRef(({ children, onScroll, className, styl
196
156
  container.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
197
157
  updateScrollbar();
198
158
  setScrollbarVisible(true);
199
- setTrackVisible(true); // 클릭 시 트랙 표시
200
- setHideTimer(2000); // 클릭 후 2초간 유지
201
- }, [updateScrollbar, setHideTimer]);
202
- // 이벤트 리스너 등록
159
+ setHideTimer(hideDelay);
160
+ }, [updateScrollbar, setHideTimer, hideDelay]);
161
+ // 위쪽 화살표 클릭 핸들러
162
+ const handleUpArrowClick = react.useCallback((event) => {
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+ if (!containerRef.current)
166
+ return;
167
+ const newScrollTop = Math.max(0, containerRef.current.scrollTop - arrowStep);
168
+ containerRef.current.scrollTop = newScrollTop;
169
+ updateScrollbar();
170
+ setScrollbarVisible(true);
171
+ setHideTimer(hideDelay);
172
+ }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
173
+ // 아래쪽 화살표 클릭 핸들러
174
+ const handleDownArrowClick = react.useCallback((event) => {
175
+ event.preventDefault();
176
+ event.stopPropagation();
177
+ if (!containerRef.current || !contentRef.current)
178
+ return;
179
+ const container = containerRef.current;
180
+ const content = contentRef.current;
181
+ const maxScrollTop = content.scrollHeight - container.clientHeight;
182
+ const newScrollTop = Math.min(maxScrollTop, container.scrollTop + arrowStep);
183
+ container.scrollTop = newScrollTop;
184
+ updateScrollbar();
185
+ setScrollbarVisible(true);
186
+ setHideTimer(hideDelay);
187
+ }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
188
+ // 스크롤 이벤트 리스너
203
189
  react.useEffect(() => {
204
190
  const container = containerRef.current;
205
191
  if (!container)
206
192
  return;
207
- // 이벤트 핸들러 (마우스 스크롤 감지)
208
- const handleWheel = () => {
209
- clearHideTimer(); // 먼저 기존 타이머 취소
210
- setScrollbarVisible(true);
211
- // 휠 스크롤 시에는 트랙 숨김 (thumb만 표시)
193
+ const handleScroll = (event) => {
212
194
  updateScrollbar();
213
- setHideTimer(700); // 0.7초 숨김
214
- };
215
- // 스크롤 이벤트 디바운스
216
- const debouncedScroll = (event) => {
217
- clearHideTimer(); // 먼저 기존 타이머 취소
195
+ // 스크롤 중에는 스크롤바 표시
196
+ clearHideTimer();
218
197
  setScrollbarVisible(true);
219
- // 스크롤 시에도 트랙 숨김 (thumb만 표시)
220
- updateScrollbar();
221
- setHideTimer(700); // 0.7초 후 숨김
198
+ // 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
199
+ const delay = isWheelScrolling ? hideDelayOnWheel : hideDelay;
200
+ setHideTimer(delay);
222
201
  if (onScroll) {
223
202
  onScroll(event);
224
203
  }
225
204
  };
226
- container.addEventListener("scroll", debouncedScroll, {
205
+ const handleWheel = () => {
206
+ // 휠 스크롤 상태 표시
207
+ setIsWheelScrolling(true);
208
+ // 기존 휠 타이머 제거
209
+ if (wheelTimeoutRef.current) {
210
+ clearTimeout(wheelTimeoutRef.current);
211
+ }
212
+ // 300ms 후 휠 스크롤 상태 해제 (휠 스크롤이 끝났다고 간주)
213
+ wheelTimeoutRef.current = setTimeout(() => {
214
+ setIsWheelScrolling(false);
215
+ }, 300);
216
+ clearHideTimer();
217
+ setScrollbarVisible(true);
218
+ };
219
+ container.addEventListener("scroll", handleScroll, {
220
+ passive: true,
221
+ });
222
+ container.addEventListener("wheel", handleWheel, {
227
223
  passive: true,
228
224
  });
229
- container.addEventListener("wheel", handleWheel, { passive: true });
230
225
  return () => {
231
- container.removeEventListener("scroll", debouncedScroll);
226
+ container.removeEventListener("scroll", handleScroll);
232
227
  container.removeEventListener("wheel", handleWheel);
228
+ if (wheelTimeoutRef.current) {
229
+ clearTimeout(wheelTimeoutRef.current);
230
+ }
233
231
  };
234
- }, [updateScrollbar, isDragging, onScroll, clearHideTimer, setHideTimer]);
235
- // 마우스 이벤트 리스너 등록 (드래그)
232
+ }, [
233
+ updateScrollbar,
234
+ onScroll,
235
+ clearHideTimer,
236
+ setHideTimer,
237
+ hideDelay,
238
+ hideDelayOnWheel,
239
+ isWheelScrolling,
240
+ ]);
241
+ // 전역 마우스 이벤트 리스너
236
242
  react.useEffect(() => {
237
243
  if (isDragging) {
238
244
  document.addEventListener("mousemove", handleMouseMove);
@@ -243,153 +249,172 @@ const OverlayScrollbar = react.forwardRef(({ children, onScroll, className, styl
243
249
  };
244
250
  }
245
251
  }, [isDragging, handleMouseMove, handleMouseUp]);
246
- // 초기 스크롤바 계산 및 스크롤 감지
252
+ // 초기 스크롤바 업데이트
247
253
  react.useEffect(() => {
248
- const checkAndUpdateScrollbar = () => {
249
- updateScrollbar();
250
- // 초기에 스크롤 가능한 콘텐츠가 있는지 확인
251
- const container = containerRef.current;
252
- const content = contentRef.current;
253
- if (container && content) {
254
- const hasScrollableContent = content.scrollHeight > container.clientHeight + 2; // 여유분 2px 추가
255
- // console.log("초기 스크롤바 체크:", {
256
- // hasScrollableContent,
257
- // contentScrollHeight: content.scrollHeight,
258
- // containerClientHeight: container.clientHeight,
259
- // });
260
- if (hasScrollableContent) {
261
- // 초기에는 스크롤바를 숨김 상태로 유지 (스크롤이나 hover 시에만 표시)
262
- setScrollbarVisible(false);
263
- setTrackVisible(false);
264
- }
265
- else {
266
- // 스크롤이 필요 없으면 확실히 숨김
267
- setScrollbarVisible(false);
268
- setTrackVisible(false);
269
- }
270
- }
271
- };
272
- // 차트 렌더링을 고려하여 더 긴 지연시간 적용
273
- const timeoutId = setTimeout(checkAndUpdateScrollbar, 200);
274
- return () => clearTimeout(timeoutId);
275
- }, [updateScrollbar, children, isDragging]);
276
- // 리사이즈 옵저버
254
+ updateScrollbar();
255
+ }, [updateScrollbar]);
256
+ // 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
277
257
  react.useEffect(() => {
278
- const container = containerRef.current;
279
- const content = contentRef.current;
280
- if (!container || !content)
258
+ const timer = setTimeout(() => {
259
+ setIsInitialized(true);
260
+ }, 100); // 100ms 후 초기화 완료
261
+ return () => clearTimeout(timer);
262
+ }, []);
263
+ // Resize observer로 크기 변경 감지
264
+ react.useEffect(() => {
265
+ if (!containerRef.current || !contentRef.current)
281
266
  return;
282
267
  const resizeObserver = new ResizeObserver(() => {
283
- // 차트 렌더링 지연을 고려하여 디바운스 적용
284
- setTimeout(() => {
285
- updateScrollbar();
286
- }, 100);
268
+ updateScrollbar();
287
269
  });
288
- resizeObserver.observe(container);
289
- resizeObserver.observe(content);
290
- return () => {
291
- resizeObserver.disconnect();
292
- };
270
+ resizeObserver.observe(containerRef.current);
271
+ resizeObserver.observe(contentRef.current);
272
+ return () => resizeObserver.disconnect();
293
273
  }, [updateScrollbar]);
294
- // 컴포넌트 언마운트 타이머 정리
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;
281
+ }
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)";
292
+ // 웹킷 스크롤바 숨기기용 CSS 동적 주입
295
293
  react.useEffect(() => {
294
+ const styleId = "overlay-scrollbar-webkit-hide";
295
+ // 이미 스타일이 있으면 제거
296
+ const existingStyle = document.getElementById(styleId);
297
+ if (existingStyle) {
298
+ existingStyle.remove();
299
+ }
300
+ const style = document.createElement("style");
301
+ style.id = styleId;
302
+ style.textContent = `
303
+ .overlay-scrollbar-container::-webkit-scrollbar {
304
+ display: none !important;
305
+ width: 0 !important;
306
+ height: 0 !important;
307
+ }
308
+ .overlay-scrollbar-container::-webkit-scrollbar-track {
309
+ display: none !important;
310
+ }
311
+ .overlay-scrollbar-container::-webkit-scrollbar-thumb {
312
+ display: none !important;
313
+ }
314
+ `;
315
+ document.head.appendChild(style);
296
316
  return () => {
297
- if (hideTimeoutRef.current) {
298
- clearTimeout(hideTimeoutRef.current);
317
+ const styleToRemove = document.getElementById(styleId);
318
+ if (styleToRemove) {
319
+ styleToRemove.remove();
299
320
  }
300
321
  };
301
322
  }, []);
302
- return (jsxRuntime.jsxs("div", { className: `overlay-scrollbar ${className || ""}`, style: Object.assign({ position: "relative", overflow: "hidden", display: "flex", flexGrow: 1, width: "100%" }, style), children: [jsxRuntime.jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
303
- display: "flex",
304
- flexGrow: 1,
305
- overflow: "auto",
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: {
324
+ width: "100%", // 명시적 너비 설정
325
+ height: "100%", // 상위 컨테이너의 전체 높이 사용
326
+ minHeight: 0, // 최소 높이 보장
327
+ overflow: "auto", // 네이티브 스크롤 기능 유지
328
+ // 브라우저 기본 스크롤바만 숨기기
306
329
  scrollbarWidth: "none", // Firefox
307
330
  msOverflowStyle: "none", // IE/Edge
308
- }, children: jsxRuntime.jsx("div", { ref: contentRef, style: { height: "100%", width: "100%" }, children: children }) }), jsxRuntime.jsx("div", { style: {
309
- position: "absolute",
310
- top: 0,
311
- right: 0,
312
- width: 20, // 20px 넓은 hover 영역
313
- height: "100%",
314
- zIndex: 5,
315
- pointerEvents: "auto",
316
- }, onMouseEnter: () => {
317
- // 스크롤 가능한 경우에만 스크롤바 표시
331
+ }, children: jsxRuntime.jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: {
332
+ minHeight: "100%",
333
+ }, children: children }) }), jsxRuntime.jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
318
334
  if (isScrollable()) {
319
335
  clearHideTimer();
320
336
  setScrollbarVisible(true);
321
- setTrackVisible(true); // hover 시 트랙까지 표시
322
337
  }
323
338
  }, onMouseLeave: () => {
324
- // 스크롤바 hover 영역에서 벗어남 시
325
339
  if (!isDragging && isScrollable()) {
326
- setTrackVisible(false); // 트랙 숨김
327
- setHideTimer(1000); // 1초 후 숨김
340
+ setHideTimer(hideDelay);
328
341
  }
329
- } }), jsxRuntime.jsx("div", { ref: scrollbarRef, onClick: handleTrackClick, onMouseEnter: () => {
330
- // 스크롤 가능한 경우에만 스크롤바 영역에 hover 시 타이머 취소하고 표시 유지
331
- if (isScrollable()) {
332
- clearHideTimer();
333
- setScrollbarVisible(true);
334
- setTrackVisible(true);
335
- }
336
- }, onMouseLeave: () => {
337
- // 스크롤바 영역에서 벗어나면 일정 시간 후 숨김
338
- if (!isDragging && isScrollable()) {
339
- setHideTimer(1000);
340
- }
341
- }, className: `overlay-scrollbar-track ${scrollbarVisible ? "visible" : ""}`, style: {
342
+ }, style: {
342
343
  position: "absolute",
343
344
  top: 0,
344
- right: 2,
345
- width: 8,
345
+ right: 0, // 완전히 오른쪽에 붙임
346
+ width: `${finalTrackWidth}px`, // hover 영역 너비
346
347
  height: "100%",
347
348
  opacity: scrollbarVisible ? 1 : 0,
348
- transition: "opacity 0.3s ease-in-out",
349
- pointerEvents: scrollbarVisible ? "auto" : "none",
350
- zIndex: 10,
349
+ transition: "opacity 0.2s ease-in-out",
351
350
  cursor: "pointer",
352
- borderRadius: "4px",
353
- // trackVisible 상태에 따라 트랙 배경 표시
354
- backgroundColor: trackVisible
355
- ? "rgba(200, 200, 200, 0.3)"
356
- : "transparent",
357
- }, onMouseOver: (e) => {
358
- e.target.style.backgroundColor =
359
- "rgba(0, 0, 0, 0.1)";
360
- }, onMouseOut: (e) => {
361
- e.target.style.backgroundColor =
362
- trackVisible
363
- ? "rgba(200, 200, 200, 0.3)"
364
- : "transparent";
365
- }, children: jsxRuntime.jsx("div", { ref: thumbRef, onMouseDown: handleThumbMouseDown, style: {
366
- position: "absolute",
367
- top: `${thumbTop}px`,
368
- left: 0,
369
- width: "100%",
370
- height: `${Math.max(thumbHeight, 30)}px`, // 최소 30px 보장
371
- backgroundColor: isDragging
372
- ? "rgba(0, 0, 0, 0.7)"
373
- : "rgba(0, 0, 0, 0.5)",
374
- borderRadius: "4px",
375
- cursor: "pointer",
376
- minHeight: "30px", // CSS로도 최소 높이 보장
377
- transition: isDragging
378
- ? "none"
379
- : "background-color 0.2s ease, transform 0.1s ease",
380
- transform: isDragging ? "scaleX(1.2)" : "scaleX(1)",
381
- opacity: isDragging ? 1 : 0.4,
382
- }, onMouseOver: (e) => {
383
- e.target.style.opacity = "1";
384
- e.target.style.transform =
385
- "scaleX(1.1)";
386
- }, onMouseOut: (e) => {
387
- if (!isDragging) {
388
- e.target.style.opacity = "0.4";
389
- e.target.style.transform =
390
- "scaleX(1)";
391
- }
392
- } }) })] }));
351
+ zIndex: 1000,
352
+ pointerEvents: "auto", // 항상 이벤트 활성화 (hover 감지용)
353
+ }, children: [jsxRuntime.jsx("div", { className: "overlay-scrollbar-track-background", onClick: handleTrackClick, style: {
354
+ position: "absolute",
355
+ top: showArrows
356
+ ? `${finalThumbWidth + 8}px`
357
+ : "4px",
358
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
359
+ width: `${finalThumbWidth}px`,
360
+ height: showArrows
361
+ ? `calc(100% - ${finalThumbWidth * 2 + 16}px)`
362
+ : "calc(100% - 8px)",
363
+ backgroundColor: trackColor,
364
+ borderRadius: `${calculatedThumbRadius}px`,
365
+ cursor: "pointer",
366
+ } }), jsxRuntime.jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
367
+ position: "absolute",
368
+ top: `${(showArrows ? finalThumbWidth + 8 : 4) +
369
+ thumbTop}px`,
370
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
371
+ width: `${finalThumbWidth}px`,
372
+ height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
373
+ backgroundColor: isDragging
374
+ ? thumbActiveColor
375
+ : thumbColor,
376
+ borderRadius: `${calculatedThumbRadius}px`,
377
+ cursor: "pointer",
378
+ transition: isDragging
379
+ ? "none"
380
+ : "background-color 0.2s ease-in-out",
381
+ } })] }), showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
382
+ position: "absolute",
383
+ top: "4px",
384
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
385
+ width: `${finalThumbWidth}px`,
386
+ height: `${finalThumbWidth}px`,
387
+ cursor: "pointer",
388
+ display: "flex",
389
+ alignItems: "center",
390
+ justifyContent: "center",
391
+ fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
392
+ color: hoveredArrow === "up"
393
+ ? finalArrowActiveColor
394
+ : finalArrowColor,
395
+ userSelect: "none",
396
+ zIndex: 1001,
397
+ opacity: scrollbarVisible ? 1 : 0,
398
+ transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
399
+ }, children: "\u25B2" })), showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
400
+ position: "absolute",
401
+ bottom: "4px",
402
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
403
+ width: `${finalThumbWidth}px`,
404
+ height: `${finalThumbWidth}px`,
405
+ cursor: "pointer",
406
+ display: "flex",
407
+ alignItems: "center",
408
+ justifyContent: "center",
409
+ fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
410
+ color: hoveredArrow === "down"
411
+ ? finalArrowActiveColor
412
+ : finalArrowColor,
413
+ userSelect: "none",
414
+ zIndex: 1001,
415
+ opacity: scrollbarVisible ? 1 : 0,
416
+ transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
417
+ }, children: "\u25BC" }))] }));
393
418
  });
394
419
 
395
420
  exports.OverlayScrollbar = OverlayScrollbar;