@designbasekorea/ui 0.1.44 → 0.1.46

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
@@ -2196,7 +2196,215 @@ const Spinner = ({ type = 'circular', size = 'm', color, speed = 1, label = '로
2196
2196
  };
2197
2197
  Spinner.displayName = 'Spinner';
2198
2198
 
2199
- const Button = React.forwardRef(({ variant = 'primary', size = 'm', radius, fullWidth = false, disabled = false, loading = false, iconOnly = false, startIcon: StartIcon, endIcon: EndIcon, className, children, onPress, type = 'button', ...props }, forwardedRef) => {
2199
+ const GAP$1 = 8; // 트리거와의 간격
2200
+ const ARW = 6; // 화살표 반쪽 길이(삼각형 변 길이)
2201
+ const PAD = 8; // 화살표가 박스 안에서 안전하게 보일 최소 여백
2202
+ const Tooltip = ({ content, children, position = 'top', size = 'm', variant = 'default', delay = 200, hideDelay = 80, alwaysShow = false, disabled = false, maxWidth = 240, showArrow = true, className, ...props }) => {
2203
+ const [visible, setVisible] = React.useState(false);
2204
+ const [style, setStyle] = React.useState({});
2205
+ const [arrowStyle, setArrowStyle] = React.useState({});
2206
+ const [placementGroup, setPlacementGroup] = React.useState('top');
2207
+ const triggerRef = React.useRef(null);
2208
+ const tooltipRef = React.useRef(null);
2209
+ const timers = React.useRef({});
2210
+ const rafId = React.useRef(null);
2211
+ const clearTimer = () => {
2212
+ if (timers.current.t) {
2213
+ clearTimeout(timers.current.t);
2214
+ timers.current.t = undefined;
2215
+ }
2216
+ };
2217
+ const groupOf = (p) => p.startsWith('top') ? 'top' :
2218
+ p.startsWith('bottom') ? 'bottom' :
2219
+ p.startsWith('left') ? 'left' : 'right';
2220
+ // 위치 계산 (⚠️ 뷰포트 클램핑 후 화살표도 보정)
2221
+ const calculatePosition = React.useCallback(() => {
2222
+ if (!triggerRef.current || !tooltipRef.current)
2223
+ return;
2224
+ const tRect = triggerRef.current.getBoundingClientRect();
2225
+ const pRect = tooltipRef.current.getBoundingClientRect();
2226
+ let top = 0, left = 0;
2227
+ // 1차: 이상적(미보정) 화살표 좌표
2228
+ let aTop = 0, aLeft = 0;
2229
+ switch (position) {
2230
+ case 'top':
2231
+ top = tRect.top - pRect.height - GAP$1;
2232
+ left = tRect.left + tRect.width / 2 - pRect.width / 2;
2233
+ aTop = pRect.height;
2234
+ aLeft = pRect.width / 2 - ARW;
2235
+ break;
2236
+ case 'top-start':
2237
+ top = tRect.top - pRect.height - GAP$1;
2238
+ left = tRect.left;
2239
+ aTop = pRect.height;
2240
+ aLeft = PAD;
2241
+ break;
2242
+ case 'top-end':
2243
+ top = tRect.top - pRect.height - GAP$1;
2244
+ left = tRect.right - pRect.width;
2245
+ aTop = pRect.height;
2246
+ aLeft = pRect.width - PAD;
2247
+ break;
2248
+ case 'bottom':
2249
+ top = tRect.bottom + GAP$1;
2250
+ left = tRect.left + tRect.width / 2 - pRect.width / 2;
2251
+ aTop = -ARW;
2252
+ aLeft = pRect.width / 2 - ARW;
2253
+ break;
2254
+ case 'bottom-start':
2255
+ top = tRect.bottom + GAP$1;
2256
+ left = tRect.left;
2257
+ aTop = -ARW;
2258
+ aLeft = PAD;
2259
+ break;
2260
+ case 'bottom-end':
2261
+ top = tRect.bottom + GAP$1;
2262
+ left = tRect.right - pRect.width;
2263
+ aTop = -ARW;
2264
+ aLeft = pRect.width - PAD;
2265
+ break;
2266
+ case 'left':
2267
+ top = tRect.top + tRect.height / 2 - pRect.height / 2;
2268
+ left = tRect.left - pRect.width - GAP$1;
2269
+ aTop = pRect.height / 2 - ARW;
2270
+ aLeft = pRect.width;
2271
+ break;
2272
+ case 'left-start':
2273
+ top = tRect.top;
2274
+ left = tRect.left - pRect.width - GAP$1;
2275
+ aTop = PAD;
2276
+ aLeft = pRect.width;
2277
+ break;
2278
+ case 'left-end':
2279
+ top = tRect.bottom - pRect.height;
2280
+ left = tRect.left - pRect.width - GAP$1;
2281
+ aTop = pRect.height - PAD;
2282
+ aLeft = pRect.width;
2283
+ break;
2284
+ case 'right':
2285
+ top = tRect.top + tRect.height / 2 - pRect.height / 2;
2286
+ left = tRect.right + GAP$1;
2287
+ aTop = pRect.height / 2 - ARW;
2288
+ aLeft = -ARW;
2289
+ break;
2290
+ case 'right-start':
2291
+ top = tRect.top;
2292
+ left = tRect.right + GAP$1;
2293
+ aTop = PAD;
2294
+ aLeft = -ARW;
2295
+ break;
2296
+ case 'right-end':
2297
+ top = tRect.bottom - pRect.height;
2298
+ left = tRect.right + GAP$1;
2299
+ aTop = pRect.height - PAD;
2300
+ aLeft = -ARW;
2301
+ break;
2302
+ }
2303
+ // 뷰포트 클램핑
2304
+ const vw = window.innerWidth;
2305
+ const vh = window.innerHeight;
2306
+ if (left < 8)
2307
+ left = 8;
2308
+ if (left + pRect.width > vw - 8)
2309
+ left = vw - pRect.width - 8;
2310
+ if (top < 8)
2311
+ top = 8;
2312
+ if (top + pRect.height > vh - 8)
2313
+ top = vh - pRect.height - 8;
2314
+ // 🔁 클램핑으로 박스 위치가 바뀌었을 수 있으니, 화살표를 박스 내부에서 다시 보정
2315
+ const g = groupOf(position);
2316
+ if (g === 'top' || g === 'bottom') {
2317
+ // 트리거 중앙 X 를 툴팁 좌표계로 변환
2318
+ const triggerCenterX = tRect.left + tRect.width / 2;
2319
+ const localX = triggerCenterX - left - ARW; // 화살표 기준점을 고려
2320
+ // 8px ~ (width-8px) 범위로 제한
2321
+ aLeft = Math.min(pRect.width - PAD, Math.max(PAD, localX));
2322
+ // aTop은 이미 위/아래에 고정(-ARW 또는 높이)
2323
+ }
2324
+ else {
2325
+ const triggerCenterY = tRect.top + tRect.height / 2;
2326
+ const localY = triggerCenterY - top - ARW;
2327
+ aTop = Math.min(pRect.height - PAD, Math.max(PAD, localY));
2328
+ // aLeft는 이미 좌/우에 고정(-ARW 또는 너비)
2329
+ }
2330
+ setStyle({
2331
+ position: 'fixed',
2332
+ top,
2333
+ left,
2334
+ maxWidth,
2335
+ zIndex: 9999,
2336
+ pointerEvents: 'none', // hover 유지
2337
+ });
2338
+ setArrowStyle({ position: 'absolute', top: aTop, left: aLeft });
2339
+ setPlacementGroup(g);
2340
+ }, [position, maxWidth]);
2341
+ // 초기 페인트 전에 위치 확정
2342
+ React.useLayoutEffect(() => {
2343
+ if (visible || alwaysShow)
2344
+ calculatePosition();
2345
+ }, [visible, alwaysShow, calculatePosition]);
2346
+ // 스크롤/리사이즈/크기변화 추적 (rAF 스로틀)
2347
+ React.useEffect(() => {
2348
+ if (!(visible || alwaysShow))
2349
+ return;
2350
+ const onMove = () => {
2351
+ if (rafId.current != null)
2352
+ cancelAnimationFrame(rafId.current);
2353
+ rafId.current = requestAnimationFrame(() => {
2354
+ calculatePosition();
2355
+ rafId.current = null;
2356
+ });
2357
+ };
2358
+ window.addEventListener('scroll', onMove, { capture: true, passive: true });
2359
+ window.addEventListener('resize', onMove, { passive: true });
2360
+ let ro = null;
2361
+ const ResizeObs = window.ResizeObserver;
2362
+ if (ResizeObs) {
2363
+ ro = new ResizeObs(() => onMove());
2364
+ if (tooltipRef.current)
2365
+ ro.observe(tooltipRef.current);
2366
+ if (triggerRef.current)
2367
+ ro.observe(triggerRef.current);
2368
+ }
2369
+ return () => {
2370
+ window.removeEventListener('scroll', onMove, { capture: true });
2371
+ window.removeEventListener('resize', onMove);
2372
+ ro?.disconnect?.();
2373
+ if (rafId.current != null)
2374
+ cancelAnimationFrame(rafId.current);
2375
+ };
2376
+ }, [visible, alwaysShow, calculatePosition]);
2377
+ // show/hide
2378
+ const show = React.useCallback(() => {
2379
+ if (disabled)
2380
+ return;
2381
+ clearTimer();
2382
+ timers.current.t = setTimeout(() => setVisible(true), Math.max(0, delay));
2383
+ }, [disabled, delay]);
2384
+ const hide = React.useCallback(() => {
2385
+ if (disabled)
2386
+ return;
2387
+ clearTimer();
2388
+ timers.current.t = setTimeout(() => setVisible(false), Math.max(0, hideDelay));
2389
+ }, [disabled, hideDelay]);
2390
+ React.useEffect(() => () => clearTimer(), []);
2391
+ React.useEffect(() => { if (!disabled && alwaysShow)
2392
+ setVisible(true);
2393
+ else if (!alwaysShow)
2394
+ setVisible(false); }, [alwaysShow, disabled]);
2395
+ const onKeyDown = React.useCallback((e) => {
2396
+ if (e.key === 'Escape') {
2397
+ clearTimer();
2398
+ setVisible(false);
2399
+ }
2400
+ }, []);
2401
+ const classes = clsx('designbase-tooltip', `designbase-tooltip--${size}`, `designbase-tooltip--${variant}`, `designbase-tooltip--${position}`, { 'designbase-tooltip--visible': visible || alwaysShow, 'designbase-tooltip--disabled': disabled }, className);
2402
+ const arrowClasses = clsx('designbase-tooltip__arrow', `designbase-tooltip__arrow--${position}`);
2403
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { ref: triggerRef, className: "designbase-tooltip__trigger", onMouseEnter: show, onMouseLeave: hide, onFocus: show, onBlur: hide, onKeyDown: onKeyDown, tabIndex: 0, "aria-describedby": visible || alwaysShow ? 'db-tooltip' : undefined, children: children }), (visible || alwaysShow) && (jsxRuntime.jsxs("div", { ref: tooltipRef, className: classes, style: style, role: "tooltip", id: "db-tooltip", "aria-hidden": !(visible || alwaysShow), "data-placement-group": placementGroup, ...props, children: [jsxRuntime.jsx("div", { className: "designbase-tooltip__content", children: content }), showArrow && jsxRuntime.jsx("div", { className: arrowClasses, style: arrowStyle })] }))] }));
2404
+ };
2405
+ Tooltip.displayName = 'Tooltip';
2406
+
2407
+ const Button = React.forwardRef(({ variant = 'primary', size = 'm', radius, fullWidth = false, disabled = false, loading = false, iconOnly = false, startIcon: StartIcon, endIcon: EndIcon, tooltip, tooltipProps, className, children, onPress, type = 'button', ...props }, forwardedRef) => {
2200
2408
  const ref = $df56164dff5785e2$export$4338b53315abf666(forwardedRef);
2201
2409
  const { buttonProps } = $701a24aa0da5b062$export$ea18c227d4417cc3({
2202
2410
  ...props,
@@ -2260,7 +2468,12 @@ const Button = React.forwardRef(({ variant = 'primary', size = 'm', radius, full
2260
2468
  }
2261
2469
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [StartIcon && (jsxRuntime.jsx(StartIcon, { size: iconSize, className: "designbase-button__start-icon", color: getIconColor() })), children, EndIcon && (jsxRuntime.jsx(EndIcon, { size: iconSize, className: "designbase-button__end-icon", color: getIconColor() }))] }));
2262
2470
  };
2263
- return (jsxRuntime.jsx("button", { ...buttonProps, ref: ref, className: classes, "aria-label": iconOnly ? props['aria-label'] || children : undefined, children: renderContent() }));
2471
+ const buttonElement = (jsxRuntime.jsx("button", { ...buttonProps, ref: ref, className: classes, "aria-label": iconOnly ? props['aria-label'] || children : undefined, children: renderContent() }));
2472
+ // 툴팁이 있는 경우 Tooltip으로 감싸기
2473
+ if (tooltip) {
2474
+ return (jsxRuntime.jsx(Tooltip, { content: tooltip, position: "top", size: "s", variant: "default", ...tooltipProps, children: buttonElement }));
2475
+ }
2476
+ return buttonElement;
2264
2477
  });
2265
2478
  Button.displayName = 'Button';
2266
2479
 
@@ -8169,9 +8382,9 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8169
8382
  }
8170
8383
  }
8171
8384
  }, [enableRecentSearches, recentSearchesKey]);
8172
- // 추천 검색어 롤링 (포커스 없이도 계속)
8385
+ // 추천 검색어 롤링 (value가 없을 때만)
8173
8386
  React.useEffect(() => {
8174
- if (suggestedSearches.length > 0 && !currentValue) {
8387
+ if (suggestedSearches.length > 0 && !currentValue && currentValue === '') {
8175
8388
  suggestionIntervalRef.current = setInterval(() => {
8176
8389
  setCurrentSuggestion(prev => (prev + 1) % suggestedSearches.length);
8177
8390
  }, suggestionRollingInterval);
@@ -8270,6 +8483,11 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8270
8483
  const handleSuggestionClick = (suggestion) => {
8271
8484
  setInternalValue(suggestion);
8272
8485
  onChange?.(suggestion);
8486
+ // 롤링 중단
8487
+ if (suggestionIntervalRef.current) {
8488
+ clearInterval(suggestionIntervalRef.current);
8489
+ suggestionIntervalRef.current = null;
8490
+ }
8273
8491
  handleSearch(suggestion);
8274
8492
  };
8275
8493
  const classes = clsx('designbase-search-bar', `designbase-search-bar--${size}`, `designbase-search-bar--${variant}`, {
@@ -8282,8 +8500,8 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8282
8500
  'designbase-search-bar__input--disabled': disabled,
8283
8501
  'designbase-search-bar__input--readonly': readOnly,
8284
8502
  });
8285
- // 현재 플레이스홀더 (추천 검색어가 있으면 롤링)
8286
- const currentPlaceholder = suggestedSearches.length > 0 && !currentValue
8503
+ // 현재 플레이스홀더 (value가 없을 때만 추천 검색어 롤링)
8504
+ const currentPlaceholder = suggestedSearches.length > 0 && !currentValue && currentValue === ''
8287
8505
  ? suggestedSearches[currentSuggestion]
8288
8506
  : placeholder;
8289
8507
  return (jsxRuntime.jsxs("div", { className: classes, role: "search", children: [jsxRuntime.jsxs("div", { className: "designbase-search-bar__container", children: [jsxRuntime.jsx("div", { className: "designbase-search-bar__search-icon", children: jsxRuntime.jsx(SearchIconComponent, { size: size === 's' ? 16 : size === 'l' ? 24 : 20 }) }), jsxRuntime.jsx("input", { ref: inputRef, type: "text", className: inputClasses, value: currentValue, placeholder: currentPlaceholder, disabled: disabled, readOnly: readOnly, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, "aria-label": "\uAC80\uC0C9\uC5B4 \uC785\uB825", ...props }), currentValue && currentValue.length > 0 && !disabled && !readOnly && (jsxRuntime.jsx("button", { type: "button", className: "designbase-search-bar__clear-button", onClick: handleClear, "aria-label": "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30", children: jsxRuntime.jsx(ClearIconComponent, { size: size === 's' ? 16 : size === 'l' ? 24 : 20 }) }))] }), showRecentSearches && recentSearches.length > 0 && (jsxRuntime.jsxs("div", { className: "designbase-search-bar__recent-searches", children: [jsxRuntime.jsxs("div", { className: "designbase-search-bar__recent-header", children: [jsxRuntime.jsx("span", { className: "designbase-search-bar__recent-title", children: "\uCD5C\uADFC \uAC80\uC0C9\uC5B4" }), jsxRuntime.jsx("button", { type: "button", className: "designbase-search-bar__clear-all-button", onClick: handleClearAllRecentSearches, "aria-label": "\uBAA8\uB4E0 \uCD5C\uADFC \uAC80\uC0C9\uC5B4 \uC0AD\uC81C", children: "\uC804\uCCB4 \uC0AD\uC81C" })] }), jsxRuntime.jsx("div", { className: "designbase-search-bar__recent-list", children: recentSearches.map((searchTerm, index) => (jsxRuntime.jsxs("div", { className: "designbase-search-bar__recent-item", children: [jsxRuntime.jsx("button", { type: "button", className: "designbase-search-bar__recent-search-button", onClick: () => handleRecentSearchClick(searchTerm), children: searchTerm }), jsxRuntime.jsx("button", { type: "button", className: "designbase-search-bar__recent-remove-button", onClick: () => handleRemoveRecentSearch(searchTerm), "aria-label": `${searchTerm} 삭제`, children: jsxRuntime.jsx(icons.CloseIcon, { size: 16 }) })] }, index))) })] })), suggestedSearches.length > 0 && isFocused && !currentValue && (jsxRuntime.jsxs("div", { className: "designbase-search-bar__suggestions", children: [jsxRuntime.jsx("div", { className: "designbase-search-bar__suggestions-header", children: jsxRuntime.jsx("span", { className: "designbase-search-bar__suggestions-title", children: "\uCD94\uCC9C \uAC80\uC0C9\uC5B4" }) }), jsxRuntime.jsx("div", { className: "designbase-search-bar__suggestions-list", children: suggestedSearches.map((suggestion, index) => (jsxRuntime.jsx("button", { type: "button", className: clsx('designbase-search-bar__suggestion-item', {
@@ -8515,7 +8733,7 @@ const Pagination = ({ currentPage, totalPages, totalItems, pageSize = 10, pageSi
8515
8733
  };
8516
8734
  Pagination.displayName = 'Pagination';
8517
8735
 
8518
- const GAP$1 = 8; // trigger와 popover 사이 간격
8736
+ const GAP = 8; // trigger와 popover 사이 간격
8519
8737
  const Popover = ({ content, children, title, position = 'top', size = 'm', variant = 'default', trigger = 'click', delay = 0, // 클릭/포커스는 즉시, 호버는 아래에서만 적용
8520
8738
  hideDelay = 80, alwaysShow = false, disabled = false, maxWidth = 300, showArrow = true, closeOnOutsideClick = true, closeOnEscape = true, open: controlledOpen, onOpenChange, className, ...props }) => {
8521
8739
  const [internalOpen, setInternalOpen] = React.useState(false);
@@ -8548,74 +8766,74 @@ hideDelay = 80, alwaysShow = false, disabled = false, maxWidth = 300, showArrow
8548
8766
  };
8549
8767
  switch (position) {
8550
8768
  case 'top':
8551
- top = tRect.top - pRect.height - GAP$1;
8769
+ top = tRect.top - pRect.height - GAP;
8552
8770
  left = tRect.left + tRect.width / 2 - pRect.width / 2;
8553
8771
  aTop = pRect.height;
8554
8772
  aLeft = pRect.width / 2 - 4;
8555
8773
  break;
8556
8774
  case 'top-start':
8557
- top = tRect.top - pRect.height - GAP$1;
8775
+ top = tRect.top - pRect.height - GAP;
8558
8776
  left = tRect.left;
8559
8777
  aTop = pRect.height;
8560
8778
  aLeft = 12;
8561
8779
  break;
8562
8780
  case 'top-end':
8563
- top = tRect.top - pRect.height - GAP$1;
8781
+ top = tRect.top - pRect.height - GAP;
8564
8782
  left = tRect.right - pRect.width;
8565
8783
  aTop = pRect.height;
8566
8784
  aLeft = pRect.width - 12;
8567
8785
  break;
8568
8786
  case 'bottom':
8569
- top = tRect.bottom + GAP$1;
8787
+ top = tRect.bottom + GAP;
8570
8788
  left = tRect.left + tRect.width / 2 - pRect.width / 2;
8571
8789
  aTop = -4;
8572
8790
  aLeft = pRect.width / 2 - 4;
8573
8791
  break;
8574
8792
  case 'bottom-start':
8575
- top = tRect.bottom + GAP$1;
8793
+ top = tRect.bottom + GAP;
8576
8794
  left = tRect.left;
8577
8795
  aTop = -4;
8578
8796
  aLeft = 12;
8579
8797
  break;
8580
8798
  case 'bottom-end':
8581
- top = tRect.bottom + GAP$1;
8799
+ top = tRect.bottom + GAP;
8582
8800
  left = tRect.right - pRect.width;
8583
8801
  aTop = -4;
8584
8802
  aLeft = pRect.width - 12;
8585
8803
  break;
8586
8804
  case 'left':
8587
8805
  top = tRect.top + tRect.height / 2 - pRect.height / 2;
8588
- left = tRect.left - pRect.width - GAP$1;
8806
+ left = tRect.left - pRect.width - GAP;
8589
8807
  aTop = pRect.height / 2 - 4;
8590
8808
  aLeft = pRect.width;
8591
8809
  break;
8592
8810
  case 'left-start':
8593
8811
  top = tRect.top;
8594
- left = tRect.left - pRect.width - GAP$1;
8812
+ left = tRect.left - pRect.width - GAP;
8595
8813
  aTop = 12;
8596
8814
  aLeft = pRect.width;
8597
8815
  break;
8598
8816
  case 'left-end':
8599
8817
  top = tRect.bottom - pRect.height;
8600
- left = tRect.left - pRect.width - GAP$1;
8818
+ left = tRect.left - pRect.width - GAP;
8601
8819
  aTop = pRect.height - 12;
8602
8820
  aLeft = pRect.width;
8603
8821
  break;
8604
8822
  case 'right':
8605
8823
  top = tRect.top + tRect.height / 2 - pRect.height / 2;
8606
- left = tRect.right + GAP$1;
8824
+ left = tRect.right + GAP;
8607
8825
  aTop = pRect.height / 2 - 4;
8608
8826
  aLeft = -4;
8609
8827
  break;
8610
8828
  case 'right-start':
8611
8829
  top = tRect.top;
8612
- left = tRect.right + GAP$1;
8830
+ left = tRect.right + GAP;
8613
8831
  aTop = 12;
8614
8832
  aLeft = -4;
8615
8833
  break;
8616
8834
  case 'right-end':
8617
8835
  top = tRect.bottom - pRect.height;
8618
- left = tRect.right + GAP$1;
8836
+ left = tRect.right + GAP;
8619
8837
  aTop = pRect.height - 12;
8620
8838
  aLeft = -4;
8621
8839
  break;
@@ -10688,214 +10906,6 @@ const Toolbar = ({ items, size = 'm', variant = 'default', position = 'top', ful
10688
10906
  return (jsxRuntime.jsx("div", { className: classes, children: jsxRuntime.jsxs("div", { className: "designbase-toolbar__content", children: [Object.entries(groupedItems).map(([groupName, groupItems], groupIndex) => (jsxRuntime.jsxs("div", { className: "designbase-toolbar__group", children: [groupItems.map(renderItem), groupIndex < Object.keys(groupedItems).length - 1 && (jsxRuntime.jsx("div", { className: "designbase-toolbar__group-separator" }))] }, groupName))), children && (jsxRuntime.jsx("div", { className: "designbase-toolbar__children", children: children }))] }) }));
10689
10907
  };
10690
10908
 
10691
- const GAP = 8; // 트리거와의 간격
10692
- const ARW = 6; // 화살표 반쪽 길이(삼각형 변 길이)
10693
- const PAD = 8; // 화살표가 박스 안에서 안전하게 보일 최소 여백
10694
- const Tooltip = ({ content, children, position = 'top', size = 'm', variant = 'default', delay = 200, hideDelay = 80, alwaysShow = false, disabled = false, maxWidth = 240, showArrow = true, className, ...props }) => {
10695
- const [visible, setVisible] = React.useState(false);
10696
- const [style, setStyle] = React.useState({});
10697
- const [arrowStyle, setArrowStyle] = React.useState({});
10698
- const [placementGroup, setPlacementGroup] = React.useState('top');
10699
- const triggerRef = React.useRef(null);
10700
- const tooltipRef = React.useRef(null);
10701
- const timers = React.useRef({});
10702
- const rafId = React.useRef(null);
10703
- const clearTimer = () => {
10704
- if (timers.current.t) {
10705
- clearTimeout(timers.current.t);
10706
- timers.current.t = undefined;
10707
- }
10708
- };
10709
- const groupOf = (p) => p.startsWith('top') ? 'top' :
10710
- p.startsWith('bottom') ? 'bottom' :
10711
- p.startsWith('left') ? 'left' : 'right';
10712
- // 위치 계산 (⚠️ 뷰포트 클램핑 후 화살표도 보정)
10713
- const calculatePosition = React.useCallback(() => {
10714
- if (!triggerRef.current || !tooltipRef.current)
10715
- return;
10716
- const tRect = triggerRef.current.getBoundingClientRect();
10717
- const pRect = tooltipRef.current.getBoundingClientRect();
10718
- let top = 0, left = 0;
10719
- // 1차: 이상적(미보정) 화살표 좌표
10720
- let aTop = 0, aLeft = 0;
10721
- switch (position) {
10722
- case 'top':
10723
- top = tRect.top - pRect.height - GAP;
10724
- left = tRect.left + tRect.width / 2 - pRect.width / 2;
10725
- aTop = pRect.height;
10726
- aLeft = pRect.width / 2 - ARW;
10727
- break;
10728
- case 'top-start':
10729
- top = tRect.top - pRect.height - GAP;
10730
- left = tRect.left;
10731
- aTop = pRect.height;
10732
- aLeft = PAD;
10733
- break;
10734
- case 'top-end':
10735
- top = tRect.top - pRect.height - GAP;
10736
- left = tRect.right - pRect.width;
10737
- aTop = pRect.height;
10738
- aLeft = pRect.width - PAD;
10739
- break;
10740
- case 'bottom':
10741
- top = tRect.bottom + GAP;
10742
- left = tRect.left + tRect.width / 2 - pRect.width / 2;
10743
- aTop = -ARW;
10744
- aLeft = pRect.width / 2 - ARW;
10745
- break;
10746
- case 'bottom-start':
10747
- top = tRect.bottom + GAP;
10748
- left = tRect.left;
10749
- aTop = -ARW;
10750
- aLeft = PAD;
10751
- break;
10752
- case 'bottom-end':
10753
- top = tRect.bottom + GAP;
10754
- left = tRect.right - pRect.width;
10755
- aTop = -ARW;
10756
- aLeft = pRect.width - PAD;
10757
- break;
10758
- case 'left':
10759
- top = tRect.top + tRect.height / 2 - pRect.height / 2;
10760
- left = tRect.left - pRect.width - GAP;
10761
- aTop = pRect.height / 2 - ARW;
10762
- aLeft = pRect.width;
10763
- break;
10764
- case 'left-start':
10765
- top = tRect.top;
10766
- left = tRect.left - pRect.width - GAP;
10767
- aTop = PAD;
10768
- aLeft = pRect.width;
10769
- break;
10770
- case 'left-end':
10771
- top = tRect.bottom - pRect.height;
10772
- left = tRect.left - pRect.width - GAP;
10773
- aTop = pRect.height - PAD;
10774
- aLeft = pRect.width;
10775
- break;
10776
- case 'right':
10777
- top = tRect.top + tRect.height / 2 - pRect.height / 2;
10778
- left = tRect.right + GAP;
10779
- aTop = pRect.height / 2 - ARW;
10780
- aLeft = -ARW;
10781
- break;
10782
- case 'right-start':
10783
- top = tRect.top;
10784
- left = tRect.right + GAP;
10785
- aTop = PAD;
10786
- aLeft = -ARW;
10787
- break;
10788
- case 'right-end':
10789
- top = tRect.bottom - pRect.height;
10790
- left = tRect.right + GAP;
10791
- aTop = pRect.height - PAD;
10792
- aLeft = -ARW;
10793
- break;
10794
- }
10795
- // 뷰포트 클램핑
10796
- const vw = window.innerWidth;
10797
- const vh = window.innerHeight;
10798
- if (left < 8)
10799
- left = 8;
10800
- if (left + pRect.width > vw - 8)
10801
- left = vw - pRect.width - 8;
10802
- if (top < 8)
10803
- top = 8;
10804
- if (top + pRect.height > vh - 8)
10805
- top = vh - pRect.height - 8;
10806
- // 🔁 클램핑으로 박스 위치가 바뀌었을 수 있으니, 화살표를 박스 내부에서 다시 보정
10807
- const g = groupOf(position);
10808
- if (g === 'top' || g === 'bottom') {
10809
- // 트리거 중앙 X 를 툴팁 좌표계로 변환
10810
- const triggerCenterX = tRect.left + tRect.width / 2;
10811
- const localX = triggerCenterX - left - ARW; // 화살표 기준점을 고려
10812
- // 8px ~ (width-8px) 범위로 제한
10813
- aLeft = Math.min(pRect.width - PAD, Math.max(PAD, localX));
10814
- // aTop은 이미 위/아래에 고정(-ARW 또는 높이)
10815
- }
10816
- else {
10817
- const triggerCenterY = tRect.top + tRect.height / 2;
10818
- const localY = triggerCenterY - top - ARW;
10819
- aTop = Math.min(pRect.height - PAD, Math.max(PAD, localY));
10820
- // aLeft는 이미 좌/우에 고정(-ARW 또는 너비)
10821
- }
10822
- setStyle({
10823
- position: 'fixed',
10824
- top,
10825
- left,
10826
- maxWidth,
10827
- zIndex: 9999,
10828
- pointerEvents: 'none', // hover 유지
10829
- });
10830
- setArrowStyle({ position: 'absolute', top: aTop, left: aLeft });
10831
- setPlacementGroup(g);
10832
- }, [position, maxWidth]);
10833
- // 초기 페인트 전에 위치 확정
10834
- React.useLayoutEffect(() => {
10835
- if (visible || alwaysShow)
10836
- calculatePosition();
10837
- }, [visible, alwaysShow, calculatePosition]);
10838
- // 스크롤/리사이즈/크기변화 추적 (rAF 스로틀)
10839
- React.useEffect(() => {
10840
- if (!(visible || alwaysShow))
10841
- return;
10842
- const onMove = () => {
10843
- if (rafId.current != null)
10844
- cancelAnimationFrame(rafId.current);
10845
- rafId.current = requestAnimationFrame(() => {
10846
- calculatePosition();
10847
- rafId.current = null;
10848
- });
10849
- };
10850
- window.addEventListener('scroll', onMove, { capture: true, passive: true });
10851
- window.addEventListener('resize', onMove, { passive: true });
10852
- let ro = null;
10853
- const ResizeObs = window.ResizeObserver;
10854
- if (ResizeObs) {
10855
- ro = new ResizeObs(() => onMove());
10856
- if (tooltipRef.current)
10857
- ro.observe(tooltipRef.current);
10858
- if (triggerRef.current)
10859
- ro.observe(triggerRef.current);
10860
- }
10861
- return () => {
10862
- window.removeEventListener('scroll', onMove, { capture: true });
10863
- window.removeEventListener('resize', onMove);
10864
- ro?.disconnect?.();
10865
- if (rafId.current != null)
10866
- cancelAnimationFrame(rafId.current);
10867
- };
10868
- }, [visible, alwaysShow, calculatePosition]);
10869
- // show/hide
10870
- const show = React.useCallback(() => {
10871
- if (disabled)
10872
- return;
10873
- clearTimer();
10874
- timers.current.t = setTimeout(() => setVisible(true), Math.max(0, delay));
10875
- }, [disabled, delay]);
10876
- const hide = React.useCallback(() => {
10877
- if (disabled)
10878
- return;
10879
- clearTimer();
10880
- timers.current.t = setTimeout(() => setVisible(false), Math.max(0, hideDelay));
10881
- }, [disabled, hideDelay]);
10882
- React.useEffect(() => () => clearTimer(), []);
10883
- React.useEffect(() => { if (!disabled && alwaysShow)
10884
- setVisible(true);
10885
- else if (!alwaysShow)
10886
- setVisible(false); }, [alwaysShow, disabled]);
10887
- const onKeyDown = React.useCallback((e) => {
10888
- if (e.key === 'Escape') {
10889
- clearTimer();
10890
- setVisible(false);
10891
- }
10892
- }, []);
10893
- const classes = clsx('designbase-tooltip', `designbase-tooltip--${size}`, `designbase-tooltip--${variant}`, `designbase-tooltip--${position}`, { 'designbase-tooltip--visible': visible || alwaysShow, 'designbase-tooltip--disabled': disabled }, className);
10894
- const arrowClasses = clsx('designbase-tooltip__arrow', `designbase-tooltip__arrow--${position}`);
10895
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { ref: triggerRef, className: "designbase-tooltip__trigger", onMouseEnter: show, onMouseLeave: hide, onFocus: show, onBlur: hide, onKeyDown: onKeyDown, tabIndex: 0, "aria-describedby": visible || alwaysShow ? 'db-tooltip' : undefined, children: children }), (visible || alwaysShow) && (jsxRuntime.jsxs("div", { ref: tooltipRef, className: classes, style: style, role: "tooltip", id: "db-tooltip", "aria-hidden": !(visible || alwaysShow), "data-placement-group": placementGroup, ...props, children: [jsxRuntime.jsx("div", { className: "designbase-tooltip__content", children: content }), showArrow && jsxRuntime.jsx("div", { className: arrowClasses, style: arrowStyle })] }))] }));
10896
- };
10897
- Tooltip.displayName = 'Tooltip';
10898
-
10899
10909
  const VideoPlayer = ({ src, poster, title, description, size = 'm', variant = 'default', theme = 'auto', autoPlay = false, loop = false, muted = false, showControls = true, enableFullscreen = true, enableKeyboard = true, enableTouch = true, showProgress = true, showTime = true, showVolume = true, showSettings = false, playlist = [], currentIndex = 0, autoPause = true, playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 2], defaultPlaybackRate = 1, qualities = [], defaultQuality = '', subtitles = [], defaultSubtitle = '', onPlay, onPause, onEnded, onTimeUpdate, onVolumeChange, onFullscreenChange, onPlaylistChange, onPlaybackRateChange, onQualityChange, onSubtitleChange, onError, className, }) => {
10900
10910
  const videoRef = React.useRef(null);
10901
10911
  const containerRef = React.useRef(null);