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