@designbasekorea/ui 0.1.45 → 0.1.47

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,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { useState, useCallback, useEffect, useRef, useMemo, useContext, useLayoutEffect, forwardRef, useId } from 'react';
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';
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, FileBlankIcon, 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
 
6
6
  function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n}
@@ -2455,7 +2455,7 @@ const Button = forwardRef(({ variant = 'primary', size = 'm', radius, fullWidth
2455
2455
  return (jsxs(Fragment, { children: [jsx(Spinner, { type: "circular", size: size === 'xs' ? 'xs' : size === 's' ? 's' : 'm', color: getIconColor(), speed: 1, showLabel: false }), !iconOnly && jsx("span", { children: "\uB85C\uB529 \uC911..." })] }));
2456
2456
  }
2457
2457
  // iconOnly 버튼일 때는 children을 아이콘으로 처리
2458
- if (iconOnly && children) {
2458
+ if (iconOnly && children && React.isValidElement(children)) {
2459
2459
  return React.cloneElement(children, {
2460
2460
  size: iconSize,
2461
2461
  color: getIconColor(),
@@ -2574,7 +2574,77 @@ const RandomGradient = ({ scheme = 'primary', tone = 'vivid', width = '100%', he
2574
2574
  };
2575
2575
  RandomGradient.displayName = 'RandomGradient';
2576
2576
 
2577
- const AdBanner = ({ type = 'hero', variant = 'primary', title, subtitle, ctaText, onCtaClick, onClose, autoClose = false, closeDelay = 5000, icon, useRandomGradient = false, gradientScheme, gradientTone = 'vivid', gradientAnimated = true, className, }) => {
2577
+ const Countdown = ({ endDate, duration, size = 'm', variant = 'default', showDays = true, showHours = true, showMinutes = true, showSeconds = true, showLabels = true, labels = {
2578
+ days: '일',
2579
+ hours: '시간',
2580
+ minutes: '분',
2581
+ seconds: '초',
2582
+ }, onComplete, onTick, className, }) => {
2583
+ // 종료 시간 계산
2584
+ const targetTime = useMemo(() => {
2585
+ if (endDate) {
2586
+ return typeof endDate === 'string' ? new Date(endDate).getTime() : endDate.getTime();
2587
+ }
2588
+ if (duration) {
2589
+ return Date.now() + duration * 1000;
2590
+ }
2591
+ return Date.now();
2592
+ }, [endDate, duration]);
2593
+ // 남은 시간 계산 함수
2594
+ const calculateTimeRemaining = () => {
2595
+ const now = Date.now();
2596
+ const total = targetTime - now;
2597
+ if (total <= 0) {
2598
+ return { days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 };
2599
+ }
2600
+ const seconds = Math.floor((total / 1000) % 60);
2601
+ const minutes = Math.floor((total / 1000 / 60) % 60);
2602
+ const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
2603
+ const days = Math.floor(total / (1000 * 60 * 60 * 24));
2604
+ return { days, hours, minutes, seconds, total };
2605
+ };
2606
+ const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining());
2607
+ useEffect(() => {
2608
+ const timer = setInterval(() => {
2609
+ const newTimeRemaining = calculateTimeRemaining();
2610
+ setTimeRemaining(newTimeRemaining);
2611
+ onTick?.(newTimeRemaining);
2612
+ if (newTimeRemaining.total <= 0) {
2613
+ clearInterval(timer);
2614
+ onComplete?.();
2615
+ }
2616
+ }, 1000);
2617
+ return () => clearInterval(timer);
2618
+ }, [targetTime, onComplete, onTick]);
2619
+ const classes = clsx('designbase-countdown', `designbase-countdown--${size}`, `designbase-countdown--${variant}`, {
2620
+ 'designbase-countdown--with-labels': showLabels,
2621
+ 'designbase-countdown--expired': timeRemaining.total <= 0,
2622
+ }, className);
2623
+ // 숫자를 두 자리로 포맷
2624
+ const formatNumber = (num) => {
2625
+ return num.toString().padStart(2, '0');
2626
+ };
2627
+ const renderTimeUnit = (value, label, show) => {
2628
+ if (!show)
2629
+ return null;
2630
+ return (jsxs("div", { className: "designbase-countdown__unit", children: [jsx("div", { className: "designbase-countdown__value", children: formatNumber(value) }), showLabels && (jsx("div", { className: "designbase-countdown__label", children: label }))] }));
2631
+ };
2632
+ const renderSeparator = () => {
2633
+ if (variant === 'minimal')
2634
+ return null;
2635
+ return jsx("div", { className: "designbase-countdown__separator", children: ":" });
2636
+ };
2637
+ const units = [
2638
+ { value: timeRemaining.days, label: labels.days || '일', show: showDays },
2639
+ { value: timeRemaining.hours, label: labels.hours || '시간', show: showHours },
2640
+ { value: timeRemaining.minutes, label: labels.minutes || '분', show: showMinutes },
2641
+ { value: timeRemaining.seconds, label: labels.seconds || '초', show: showSeconds },
2642
+ ].filter(unit => unit.show);
2643
+ return (jsx("div", { className: classes, children: units.map((unit, index) => (jsxs(React.Fragment, { children: [renderTimeUnit(unit.value, unit.label, unit.show), index < units.length - 1 && renderSeparator()] }, unit.label))) }));
2644
+ };
2645
+ Countdown.displayName = 'Countdown';
2646
+
2647
+ const AdBanner = ({ type = 'hero', variant = 'primary', title, subtitle, ctaText, onCtaClick, onClose, autoClose = false, closeDelay = 5000, icon, useRandomGradient = false, gradientScheme, gradientTone = 'vivid', gradientAnimated = true, countdownEndDate, showCountdown = false, className, }) => {
2578
2648
  const [isVisible, setIsVisible] = useState(true);
2579
2649
  const [progress, setProgress] = useState(100);
2580
2650
  useEffect(() => {
@@ -2605,6 +2675,7 @@ const AdBanner = ({ type = 'hero', variant = 'primary', title, subtitle, ctaText
2605
2675
  const classes = clsx('designbase-ad-banner', `designbase-ad-banner--type-${type}`, `designbase-ad-banner--variant-${variant}`, {
2606
2676
  'designbase-ad-banner--auto-close': autoClose,
2607
2677
  'designbase-ad-banner--with-gradient': useRandomGradient,
2678
+ 'designbase-ad-banner--gradient-light': useRandomGradient && gradientTone === 'light',
2608
2679
  }, className);
2609
2680
  // 기본 아이콘
2610
2681
  const defaultIcons = {
@@ -2627,12 +2698,13 @@ const AdBanner = ({ type = 'hero', variant = 'primary', title, subtitle, ctaText
2627
2698
  success: 'success',
2628
2699
  warning: 'warning',
2629
2700
  error: 'error',
2630
- gradient: 'sunset',
2631
2701
  };
2632
2702
  return schemeMap[variant];
2633
2703
  };
2634
2704
  // 배너 컨텐츠
2635
- const bannerContent = (jsxs("div", { className: "designbase-ad-banner__content", children: [jsx("button", { className: "designbase-ad-banner__close", onClick: handleClose, "aria-label": "\uB2EB\uAE30", children: jsx(CloseIcon, { size: type === 'hero' ? 20 : 18 }) }), type === 'hero' && !useRandomGradient && (jsxs(Fragment, { children: [jsx("div", { className: "designbase-ad-banner__decoration designbase-ad-banner__decoration--top" }), jsx("div", { className: "designbase-ad-banner__decoration designbase-ad-banner__decoration--bottom" })] })), jsxs("div", { className: "designbase-ad-banner__main", children: [jsxs("div", { className: "designbase-ad-banner__icon", children: [displayIcon, type === 'topbar' && (jsx("span", { className: "designbase-ad-banner__label", children: "\uD2B9\uBCC4 \uC774\uBCA4\uD2B8" })), type === 'floating' && (jsx("span", { className: "designbase-ad-banner__badge", children: "HOT DEAL" }))] }), jsxs("div", { className: "designbase-ad-banner__text", children: [jsx("h2", { className: "designbase-ad-banner__title", children: displayTitle }), jsx("p", { className: "designbase-ad-banner__subtitle", children: displaySubtitle })] }), jsx("div", { className: "designbase-ad-banner__actions", children: jsxs(Button, { variant: "primary", size: type === 'hero' ? 'l' : type === 'card' ? 's' : 'm', onPress: handleCtaClick, className: "designbase-ad-banner__cta", fullWidth: type === 'floating', children: [displayCtaText, jsx(ArrowRightIcon, { size: type === 'hero' ? 20 : 16 })] }) })] }), autoClose && (jsx("div", { className: "designbase-ad-banner__progress", children: jsx("div", { className: "designbase-ad-banner__progress-bar", style: { width: `${progress}%` } }) }))] }));
2705
+ const bannerContent = (jsxs("div", { className: "designbase-ad-banner__content", children: [jsx("button", { className: "designbase-ad-banner__close", onClick: handleClose, "aria-label": "\uB2EB\uAE30", children: jsx(CloseIcon, { size: 20 }) }), jsxs("div", { className: "designbase-ad-banner__main", children: [jsxs("div", { className: "designbase-ad-banner__icon", children: [displayIcon, type === 'topbar' && (jsx("span", { className: "designbase-ad-banner__label", children: "\uD2B9\uBCC4 \uC774\uBCA4\uD2B8" })), type === 'floating' && (jsx("span", { className: "designbase-ad-banner__badge", children: "HOT DEAL" }))] }), jsxs("div", { className: "designbase-ad-banner__text", children: [jsx("h2", { className: "designbase-ad-banner__title", children: displayTitle }), jsx("p", { className: "designbase-ad-banner__subtitle", children: displaySubtitle }), showCountdown && countdownEndDate && (jsx("div", { className: "designbase-ad-banner__countdown", children: jsx(Countdown, { endDate: countdownEndDate, size: "s", variant: "compact", showLabels: false, onComplete: () => {
2706
+ console.log('카운트다운 완료!');
2707
+ } }) }))] }), jsx("div", { className: "designbase-ad-banner__actions", children: jsxs(Button, { variant: "primary", size: type === 'hero' ? 'l' : type === 'card' ? 's' : 'm', onPress: handleCtaClick, className: "designbase-ad-banner__cta", fullWidth: type === 'floating', children: [displayCtaText, jsx(ArrowRightIcon, { size: type === 'hero' ? 20 : 16 })] }) })] }), autoClose && (jsx("div", { className: "designbase-ad-banner__progress", children: jsx("div", { className: "designbase-ad-banner__progress-bar", style: { width: `${progress}%` } }) }))] }));
2636
2708
  return (jsx("div", { className: classes, children: useRandomGradient ? (jsx(RandomGradient, { scheme: getGradientScheme(), tone: gradientTone, animated: gradientAnimated, height: "100%", width: "100%", className: "designbase-ad-banner__gradient-bg", children: bannerContent })) : (bannerContent) }));
2637
2709
  };
2638
2710
  // 기본 텍스트 헬퍼
@@ -4772,6 +4844,58 @@ const Card = forwardRef(function Card({ title, subtitle, description, children,
4772
4844
  });
4773
4845
  Card.displayName = 'Card';
4774
4846
 
4847
+ const Indicator = ({ current, total, type = 'dots', direction = 'horizontal', size = 'm', clickable = false, onStepClick, timer = false, timerDuration = 3000, onTimerComplete, className, ...props }) => {
4848
+ const [progress, setProgress] = useState(0);
4849
+ const timerRef = useRef(null);
4850
+ const startRef = useRef(0);
4851
+ useEffect(() => {
4852
+ if (!timer)
4853
+ return;
4854
+ setProgress(0);
4855
+ startRef.current = Date.now();
4856
+ const animate = () => {
4857
+ const elapsed = Date.now() - startRef.current;
4858
+ const ratio = Math.min(elapsed / timerDuration, 1);
4859
+ setProgress(ratio);
4860
+ if (ratio < 1) {
4861
+ timerRef.current = requestAnimationFrame(animate);
4862
+ }
4863
+ else {
4864
+ onTimerComplete?.();
4865
+ }
4866
+ };
4867
+ timerRef.current = requestAnimationFrame(animate);
4868
+ return () => {
4869
+ if (timerRef.current)
4870
+ cancelAnimationFrame(timerRef.current);
4871
+ };
4872
+ }, [current, timer, timerDuration, onTimerComplete]);
4873
+ const handleStepClick = (i) => {
4874
+ if (clickable && onStepClick)
4875
+ onStepClick(i);
4876
+ };
4877
+ const classes = clsx('designbase-indicator', `designbase-indicator--${type}`, `designbase-indicator--${direction}`, `designbase-indicator--${size}`, { 'designbase-indicator--clickable': clickable }, className);
4878
+ // 숫자 타입
4879
+ if (type === 'numbers') {
4880
+ return (jsxs("div", { className: classes, role: "progressbar", "aria-valuenow": current + 1, "aria-valuemin": 1, "aria-valuemax": total, children: [jsx("span", { className: "designbase-indicator__current", children: current + 1 }), jsx("span", { className: "designbase-indicator__separator", children: "/" }), jsx("span", { className: "designbase-indicator__total", children: total })] }));
4881
+ }
4882
+ // 라인 타입 (가로 progress bar)
4883
+ if (type === 'line') {
4884
+ return (jsx("div", { className: classes, role: "progressbar", "aria-valuenow": current + 1, "aria-valuemin": 1, "aria-valuemax": total, children: Array.from({ length: total }, (_, i) => (jsx("div", { className: clsx('designbase-indicator__line-segment', {
4885
+ 'designbase-indicator__line-segment--active': i < current,
4886
+ 'designbase-indicator__line-segment--current': i === current
4887
+ }), children: timer && i === current && (jsx("div", { className: "designbase-indicator__line-fill", style: { transform: `scaleX(${progress})` } })) }, i))) }));
4888
+ }
4889
+ // 점(dot) 타입 + 원형 애니메이션
4890
+ return (jsx("div", { className: classes, role: "progressbar", "aria-valuenow": current + 1, "aria-valuemin": 1, "aria-valuemax": total, children: Array.from({ length: total }, (_, i) => {
4891
+ const active = i === current;
4892
+ const circumference = 2 * Math.PI * 15.9155;
4893
+ const offset = circumference * (1 - progress);
4894
+ return (jsx("button", { className: clsx('designbase-indicator__dot', { 'designbase-indicator__dot--active': active }), onClick: () => handleStepClick(i), disabled: !clickable, children: timer && active && (jsxs("svg", { className: "designbase-indicator__circle", viewBox: "0 0 36 36", children: [jsx("circle", { className: "designbase-indicator__circle-bg", cx: "18", cy: "18", r: "15.9155", fill: "none", stroke: "var(--db-border-base)", strokeWidth: "2" }), jsx("circle", { className: "designbase-indicator__circle-progress", cx: "18", cy: "18", r: "15.9155", fill: "none", stroke: "var(--db-brand-primary)", strokeWidth: "2", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round" })] })) }, i));
4895
+ }) }));
4896
+ };
4897
+ Indicator.displayName = 'Indicator';
4898
+
4775
4899
  const Carousel = ({ items, size = 'm', variant = 'default', theme = 'light', transition = 'slide', autoPlay = false, autoPlayInterval = 5000, infinite = true, itemsPerView = 1, gap = 16, showNavigation = true, showIndicators = true, indicatorStyle = 'dots', showAutoPlayControl = true, showTitle = true, showDescription = true, enableTouch = true, enableKeyboard = true, enableWheel = false, enableFullscreen = false, enableDownload = false, enableShare = false, enableLike = false, enableBookmark = false, readonly = false, disabled = false, enablePaging = false, swipeSensitivity = 50, transitionDuration = 300, onItemClick, onSlideChange, onLike, onBookmark, onShare, onDownload, onFullscreenChange, className, }) => {
4776
4900
  // 아이콘 크기 계산 (m이 기본값)
4777
4901
  const iconSize = size === 's' ? 16 : size === 'l' ? 24 : size === 'xl' ? 28 : 20;
@@ -5040,10 +5164,6 @@ const Carousel = ({ items, size = 'm', variant = 'default', theme = 'light', tra
5040
5164
  willChange: isDragging ? 'transform' : 'auto',
5041
5165
  };
5042
5166
  };
5043
- // 인디케이터 활성 상태 확인
5044
- const isIndicatorActive = (index) => {
5045
- return index === currentIndex;
5046
- };
5047
5167
  // 네비게이션 버튼 비활성화 상태
5048
5168
  const isPrevDisabled = !infinite && currentIndex === 0;
5049
5169
  const isNextDisabled = !infinite && currentIndex === items.length - 1;
@@ -5082,9 +5202,7 @@ const Carousel = ({ items, size = 'm', variant = 'default', theme = 'light', tra
5082
5202
  }, title: "\uACF5\uC720", children: jsx(ShareAltIcon, { size: iconSize, color: "currentColor" }) })), enableDownload && item.image && (jsx("button", { className: "designbase-carousel__action-button designbase-carousel__action-button--download", onClick: (e) => {
5083
5203
  e.stopPropagation();
5084
5204
  handleDownload(item, index);
5085
- }, title: "\uB2E4\uC6B4\uB85C\uB4DC", children: jsx(DownloadIcon, { size: iconSize, color: "currentColor" }) }))] })] }, item.id))) }), showNavigation && items.length > 1 && (jsxs(Fragment, { children: [jsx("button", { className: clsx('designbase-carousel__nav-button', 'designbase-carousel__nav-button--prev'), onClick: goToPrevious, disabled: disabled || readonly || isPrevDisabled, title: "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ChevronLeftIcon, { size: iconSize, color: "currentColor" }) }), jsx("button", { className: clsx('designbase-carousel__nav-button', 'designbase-carousel__nav-button--next'), onClick: goToNext, disabled: disabled || readonly || isNextDisabled, title: "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ChevronRightIcon, { size: iconSize, color: "currentColor" }) })] })), enablePaging && items.length > 1 && (jsxs(Fragment, { children: [jsx("button", { className: "designbase-carousel__quick-nav-button designbase-carousel__quick-nav-button--first", onClick: goToFirst, disabled: disabled || readonly || currentIndex === 0, title: "\uCCAB \uBC88\uC9F8 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ArrowLeftIcon, { size: iconSize, color: "currentColor" }) }), jsx("button", { className: "designbase-carousel__quick-nav-button designbase-carousel__quick-nav-button--last", onClick: goToLast, disabled: disabled || readonly || currentIndex === items.length - 1, title: "\uB9C8\uC9C0\uB9C9 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ArrowRightIcon, { size: iconSize, color: "currentColor" }) })] })), enableFullscreen && (jsx("button", { className: "designbase-carousel__fullscreen-button", onClick: handleFullscreenToggle, disabled: disabled || readonly, title: isFullscreen ? "전체화면 해제" : "전체화면", type: "button", children: isFullscreen ? jsx(ShrinkIcon, { size: iconSize, color: "currentColor" }) : jsx(ExpandIcon, { size: iconSize, color: "currentColor" }) }))] }), jsxs("div", { className: "designbase-carousel__controls", children: [showIndicators && items.length > 1 && (jsx("div", { className: "designbase-carousel__indicators", children: items.map((item, index) => (jsx("button", { className: clsx('designbase-carousel__indicator', {
5086
- 'designbase-carousel__indicator--active': isIndicatorActive(index),
5087
- }), onClick: () => goToSlide(index), disabled: disabled || readonly, title: `슬라이드 ${index + 1}로 이동`, type: "button", children: indicatorStyle === 'thumbnails' && item.thumbnail ? (jsx("img", { src: item.thumbnail, alt: item.title || `썸네일 ${index + 1}`, className: "designbase-carousel__indicator-thumbnail" })) : indicatorStyle === 'numbers' ? (jsx("span", { className: "designbase-carousel__indicator-number", children: index + 1 })) : indicatorStyle === 'lines' ? (jsx("span", { className: "designbase-carousel__indicator-line" })) : (jsx("span", { className: "designbase-carousel__indicator-dot" })) }, index))) })), showAutoPlayControl && items.length > 1 && (jsx("button", { className: "designbase-carousel__autoplay-button", onClick: handleAutoPlayToggle, disabled: disabled || readonly, title: isAutoPlaying ? "자동 재생 정지" : "자동 재생 시작", type: "button", children: isAutoPlaying ? jsx(PauseIcon, { size: iconSize, color: "currentColor" }) : jsx(PlayIcon, { size: iconSize, color: "currentColor" }) }))] })] }));
5205
+ }, title: "\uB2E4\uC6B4\uB85C\uB4DC", children: jsx(DownloadIcon, { size: iconSize, color: "currentColor" }) }))] })] }, item.id))) }), showNavigation && items.length > 1 && (jsxs(Fragment, { children: [jsx("button", { className: clsx('designbase-carousel__nav-button', 'designbase-carousel__nav-button--prev'), onClick: goToPrevious, disabled: disabled || readonly || isPrevDisabled, title: "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ChevronLeftIcon, { size: iconSize, color: "currentColor" }) }), jsx("button", { className: clsx('designbase-carousel__nav-button', 'designbase-carousel__nav-button--next'), onClick: goToNext, disabled: disabled || readonly || isNextDisabled, title: "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ChevronRightIcon, { size: iconSize, color: "currentColor" }) })] })), enablePaging && items.length > 1 && (jsxs(Fragment, { children: [jsx("button", { className: "designbase-carousel__quick-nav-button designbase-carousel__quick-nav-button--first", onClick: goToFirst, disabled: disabled || readonly || currentIndex === 0, title: "\uCCAB \uBC88\uC9F8 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ArrowLeftIcon, { size: iconSize, color: "currentColor" }) }), jsx("button", { className: "designbase-carousel__quick-nav-button designbase-carousel__quick-nav-button--last", onClick: goToLast, disabled: disabled || readonly || currentIndex === items.length - 1, title: "\uB9C8\uC9C0\uB9C9 \uC2AC\uB77C\uC774\uB4DC", type: "button", children: jsx(ArrowRightIcon, { size: iconSize, color: "currentColor" }) })] })), enableFullscreen && (jsx("button", { className: "designbase-carousel__fullscreen-button", onClick: handleFullscreenToggle, disabled: disabled || readonly, title: isFullscreen ? "전체화면 해제" : "전체화면", type: "button", children: isFullscreen ? jsx(ShrinkIcon, { size: iconSize, color: "currentColor" }) : jsx(ExpandIcon, { size: iconSize, color: "currentColor" }) }))] }), jsxs("div", { className: "designbase-carousel__controls", children: [showIndicators && items.length > 1 && (jsx("div", { className: "designbase-carousel__indicators", children: jsx(Indicator, { current: currentIndex, total: items.length, type: indicatorStyle === 'lines' ? 'line' : indicatorStyle === 'numbers' ? 'numbers' : 'dots', size: "s", clickable: true, onStepClick: goToSlide }) })), showAutoPlayControl && items.length > 1 && (jsx("button", { className: "designbase-carousel__autoplay-button", onClick: handleAutoPlayToggle, disabled: disabled || readonly, title: isAutoPlaying ? "자동 재생 정지" : "자동 재생 시작", type: "button", children: isAutoPlaying ? jsx(PauseIcon, { size: iconSize, color: "currentColor" }) : jsx(PlayIcon, { size: iconSize, color: "currentColor" }) }))] })] }));
5088
5206
  };
5089
5207
 
5090
5208
  const Chip = ({ label, size = 'm', variant = 'default', color = 'primary', deletable = false, onDelete, onClick, startIcon, endIcon, disabled = false, selected = false, fullWidth = false, className, ...props }) => {
@@ -5502,76 +5620,6 @@ const Confirm = ({ open, title, children, confirmText = '확인', cancelText = '
5502
5620
  };
5503
5621
  Confirm.displayName = 'Confirm';
5504
5622
 
5505
- const Countdown = ({ endDate, duration, size = 'm', variant = 'default', showDays = true, showHours = true, showMinutes = true, showSeconds = true, showLabels = true, labels = {
5506
- days: '일',
5507
- hours: '시간',
5508
- minutes: '분',
5509
- seconds: '초',
5510
- }, onComplete, onTick, className, }) => {
5511
- // 종료 시간 계산
5512
- const targetTime = useMemo(() => {
5513
- if (endDate) {
5514
- return typeof endDate === 'string' ? new Date(endDate).getTime() : endDate.getTime();
5515
- }
5516
- if (duration) {
5517
- return Date.now() + duration * 1000;
5518
- }
5519
- return Date.now();
5520
- }, [endDate, duration]);
5521
- // 남은 시간 계산 함수
5522
- const calculateTimeRemaining = () => {
5523
- const now = Date.now();
5524
- const total = targetTime - now;
5525
- if (total <= 0) {
5526
- return { days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 };
5527
- }
5528
- const seconds = Math.floor((total / 1000) % 60);
5529
- const minutes = Math.floor((total / 1000 / 60) % 60);
5530
- const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
5531
- const days = Math.floor(total / (1000 * 60 * 60 * 24));
5532
- return { days, hours, minutes, seconds, total };
5533
- };
5534
- const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining());
5535
- useEffect(() => {
5536
- const timer = setInterval(() => {
5537
- const newTimeRemaining = calculateTimeRemaining();
5538
- setTimeRemaining(newTimeRemaining);
5539
- onTick?.(newTimeRemaining);
5540
- if (newTimeRemaining.total <= 0) {
5541
- clearInterval(timer);
5542
- onComplete?.();
5543
- }
5544
- }, 1000);
5545
- return () => clearInterval(timer);
5546
- }, [targetTime, onComplete, onTick]);
5547
- const classes = clsx('designbase-countdown', `designbase-countdown--${size}`, `designbase-countdown--${variant}`, {
5548
- 'designbase-countdown--with-labels': showLabels,
5549
- 'designbase-countdown--expired': timeRemaining.total <= 0,
5550
- }, className);
5551
- // 숫자를 두 자리로 포맷
5552
- const formatNumber = (num) => {
5553
- return num.toString().padStart(2, '0');
5554
- };
5555
- const renderTimeUnit = (value, label, show) => {
5556
- if (!show)
5557
- return null;
5558
- return (jsxs("div", { className: "designbase-countdown__unit", children: [jsx("div", { className: "designbase-countdown__value", children: formatNumber(value) }), showLabels && (jsx("div", { className: "designbase-countdown__label", children: label }))] }));
5559
- };
5560
- const renderSeparator = () => {
5561
- if (variant === 'minimal')
5562
- return null;
5563
- return jsx("div", { className: "designbase-countdown__separator", children: ":" });
5564
- };
5565
- const units = [
5566
- { value: timeRemaining.days, label: labels.days || '일', show: showDays },
5567
- { value: timeRemaining.hours, label: labels.hours || '시간', show: showHours },
5568
- { value: timeRemaining.minutes, label: labels.minutes || '분', show: showMinutes },
5569
- { value: timeRemaining.seconds, label: labels.seconds || '초', show: showSeconds },
5570
- ].filter(unit => unit.show);
5571
- return (jsx("div", { className: classes, children: units.map((unit, index) => (jsxs(React.Fragment, { children: [renderTimeUnit(unit.value, unit.label, unit.show), index < units.length - 1 && renderSeparator()] }, unit.label))) }));
5572
- };
5573
- Countdown.displayName = 'Countdown';
5574
-
5575
5623
  const Container = ({ size = 'l', maxWidth, variant = 'plain', padding = 'm', margin = 'none', backgroundColor, border = false, rounded = false, shadow = false, className, children, }) => {
5576
5624
  // 크기별 최대 폭 설정 (토큰 기반)
5577
5625
  const getMaxWidth = () => {
@@ -6590,67 +6638,70 @@ Progressbar.displayName = 'Progressbar';
6590
6638
  const FileUploader = ({ size = 'm', variant = 'default', accept, maxSize, multiple = false, showFileList = true, showProgress = true, disabled = false, readonly = false, onUpload, onRemove, onRetry, className, }) => {
6591
6639
  const [uploadedFiles, setUploadedFiles] = useState([]);
6592
6640
  const [isUploading, setIsUploading] = useState(false);
6593
- // 파일 선택 처리
6594
- const handleFileSelect = useCallback((files) => {
6595
- if (disabled || readonly)
6596
- return;
6597
- const newUploadFiles = files.map(file => ({
6598
- file,
6599
- id: `${Date.now()}-${Math.random()}`,
6600
- status: 'pending',
6601
- progress: 0,
6602
- }));
6603
- if (multiple) {
6604
- setUploadedFiles(prev => [...prev, ...newUploadFiles]);
6605
- }
6606
- else {
6607
- setUploadedFiles(newUploadFiles);
6608
- }
6609
- // 시뮬레이션된 업로드 진행
6610
- simulateUpload(newUploadFiles);
6611
- }, [disabled, readonly, multiple]);
6612
- // 업로드 시뮬레이션
6641
+ // ────────────────────────────────────────────────
6642
+ // 업로드 시뮬레이션 (먼저 정의해야 함!)
6643
+ // ────────────────────────────────────────────────
6613
6644
  const simulateUpload = useCallback((files) => {
6614
6645
  setIsUploading(true);
6615
6646
  files.forEach((uploadFile, index) => {
6616
6647
  // 업로드 시작
6617
- setUploadedFiles(prev => prev.map(f => f.id === uploadFile.id
6618
- ? { ...f, status: 'uploading' }
6619
- : f));
6620
- // 진행률 시뮬레이션
6648
+ setUploadedFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'uploading' } : f));
6621
6649
  let progress = 0;
6622
6650
  const interval = setInterval(() => {
6623
6651
  progress += Math.random() * 20;
6624
6652
  if (progress >= 100) {
6625
6653
  progress = 100;
6626
6654
  clearInterval(interval);
6627
- // 업로드 완료
6655
+ // 완료 처리
6628
6656
  setTimeout(() => {
6629
6657
  setUploadedFiles(prev => prev.map(f => f.id === uploadFile.id
6630
6658
  ? {
6631
6659
  ...f,
6632
6660
  status: 'success',
6633
6661
  progress: 100,
6634
- uploadedAt: new Date()
6662
+ uploadedAt: new Date(),
6635
6663
  }
6636
6664
  : f));
6637
- // 모든 파일 업로드 완료 확인
6665
+ // 모든 업로드 완료 확인
6638
6666
  setTimeout(() => {
6639
- const allCompleted = uploadedFiles.every(f => f.status === 'success');
6640
- if (allCompleted) {
6641
- setIsUploading(false);
6642
- }
6643
- }, 500);
6667
+ setUploadedFiles(prev => {
6668
+ const allDone = prev.every(f => f.status === 'success');
6669
+ if (allDone)
6670
+ setIsUploading(false);
6671
+ return prev;
6672
+ });
6673
+ }, 300);
6644
6674
  }, 200);
6645
6675
  }
6646
6676
  else {
6647
- setUploadedFiles(prev => prev.map(f => f.id === uploadFile.id
6648
- ? { ...f, progress }
6649
- : f));
6677
+ setUploadedFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f));
6650
6678
  }
6651
- }, 100 + index * 50); // 각 파일마다 약간의 지연
6679
+ }, 100 + index * 50);
6652
6680
  });
6653
- }, [uploadedFiles]);
6681
+ }, []);
6682
+ // ────────────────────────────────────────────────
6683
+ // 파일 선택 시 처리
6684
+ // ────────────────────────────────────────────────
6685
+ const handleFileSelect = useCallback((files) => {
6686
+ if (disabled || readonly)
6687
+ return;
6688
+ const newUploadFiles = files.map(file => ({
6689
+ file,
6690
+ id: `${Date.now()}-${Math.random()}`,
6691
+ status: 'pending',
6692
+ progress: 0,
6693
+ }));
6694
+ if (multiple) {
6695
+ setUploadedFiles(prev => [...prev, ...newUploadFiles]);
6696
+ }
6697
+ else {
6698
+ setUploadedFiles(newUploadFiles);
6699
+ }
6700
+ // ✅ 부모 콜백 즉시 실행 (파일 업로드 감지용)
6701
+ onUpload?.(newUploadFiles);
6702
+ // 진행률 애니메이션
6703
+ simulateUpload(newUploadFiles);
6704
+ }, [disabled, readonly, multiple, onUpload, simulateUpload]);
6654
6705
  // 파일 제거
6655
6706
  const handleRemove = useCallback((fileId) => {
6656
6707
  if (disabled || readonly)
@@ -6658,17 +6709,16 @@ const FileUploader = ({ size = 'm', variant = 'default', accept, maxSize, multip
6658
6709
  setUploadedFiles(prev => prev.filter(f => f.id !== fileId));
6659
6710
  onRemove?.(fileId);
6660
6711
  }, [disabled, readonly, onRemove]);
6661
- // 파일 재시도
6712
+ // 재시도
6662
6713
  const handleRetry = useCallback((fileId) => {
6663
6714
  if (disabled || readonly)
6664
6715
  return;
6665
6716
  const file = uploadedFiles.find(f => f.id === fileId);
6666
- if (file) {
6717
+ if (file)
6667
6718
  simulateUpload([file]);
6668
- }
6669
6719
  onRetry?.(fileId);
6670
6720
  }, [disabled, readonly, uploadedFiles, simulateUpload, onRetry]);
6671
- // 파일 크기 포맷팅
6721
+ // 파일 크기 표시
6672
6722
  const formatFileSize = useCallback((bytes) => {
6673
6723
  if (bytes === 0)
6674
6724
  return '0 Bytes';
@@ -6681,7 +6731,7 @@ const FileUploader = ({ size = 'm', variant = 'default', accept, maxSize, multip
6681
6731
  const getStatusIcon = useCallback((status) => {
6682
6732
  switch (status) {
6683
6733
  case 'pending':
6684
- return jsx(ClockIcon, { size: 20 });
6734
+ return jsx(FileBlankIcon, { size: 20 });
6685
6735
  case 'uploading':
6686
6736
  return jsx(Spinner, { type: "circular", size: "s" });
6687
6737
  case 'success':
@@ -6692,10 +6742,13 @@ const FileUploader = ({ size = 'm', variant = 'default', accept, maxSize, multip
6692
6742
  return null;
6693
6743
  }
6694
6744
  }, []);
6745
+ // ────────────────────────────────────────────────
6746
+ // 렌더링
6747
+ // ────────────────────────────────────────────────
6695
6748
  return (jsxs("div", { className: clsx('designbase-file-uploader', `designbase-file-uploader--size-${size}`, `designbase-file-uploader--variant-${variant}`, {
6696
6749
  'designbase-file-uploader--disabled': disabled,
6697
6750
  'designbase-file-uploader--readonly': readonly,
6698
- }, className), children: [jsx(Dropzone, { size: size, variant: variant, accept: accept, maxSize: maxSize, multiple: multiple, disabled: disabled, readonly: readonly, onFileSelect: handleFileSelect }), showFileList && uploadedFiles.length > 0 && (jsxs("div", { className: "designbase-file-uploader__file-list", children: [jsxs("div", { className: "designbase-file-uploader__file-list-title", children: ["\uC5C5\uB85C\uB4DC\uB41C \uD30C\uC77C\uB4E4 (", uploadedFiles.length, "\uAC1C)"] }), jsx("div", { className: "designbase-file-uploader__file-items", children: uploadedFiles.map((uploadFile) => (jsxs("div", { className: clsx('designbase-file-uploader__file-item', `designbase-file-uploader__file-item--${uploadFile.status}`), children: [jsxs("div", { className: "designbase-file-uploader__file-info", children: [jsx("div", { className: "designbase-file-uploader__file-icon", children: getStatusIcon(uploadFile.status) }), jsxs("div", { className: "designbase-file-uploader__file-details", children: [jsx("div", { className: "designbase-file-uploader__file-name", children: uploadFile.file.name }), jsx("div", { className: "designbase-file-uploader__file-size", children: formatFileSize(uploadFile.file.size) }), uploadFile.error && (jsx("div", { className: "designbase-file-uploader__file-error", children: uploadFile.error }))] })] }), showProgress && uploadFile.status === 'uploading' && (jsx("div", { className: "designbase-file-uploader__progress", children: jsx(Progressbar, { value: uploadFile.progress || 0, size: "s", variant: "primary", style: "solid", showLabel: true, labelPosition: "inside", fullWidth: true }) })), jsxs("div", { className: "designbase-file-uploader__file-actions", children: [uploadFile.status === 'error' && (jsx("button", { className: "designbase-file-uploader__retry-button", onClick: () => handleRetry(uploadFile.id), disabled: disabled || readonly, type: "button", children: "\uC7AC\uC2DC\uB3C4" })), jsx("button", { className: "designbase-file-uploader__remove-button", onClick: () => handleRemove(uploadFile.id), disabled: disabled || readonly, type: "button", children: "\uC0AD\uC81C" })] })] }, uploadFile.id))) })] }))] }));
6751
+ }, className), children: [jsx(Dropzone, { size: size, variant: variant, accept: accept, maxSize: maxSize, multiple: multiple, disabled: disabled, readonly: readonly, onFileSelect: handleFileSelect }), showFileList && uploadedFiles.length > 0 && (jsxs("div", { className: "designbase-file-uploader__file-list", children: [jsxs("div", { className: "designbase-file-uploader__file-list-title", children: ["\uC5C5\uB85C\uB4DC\uB41C \uD30C\uC77C\uB4E4 (", uploadedFiles.length, "\uAC1C)"] }), jsx("div", { className: "designbase-file-uploader__file-items", children: uploadedFiles.map(uploadFile => (jsxs("div", { className: clsx('designbase-file-uploader__file-item', `designbase-file-uploader__file-item--${uploadFile.status}`), children: [jsxs("div", { className: "designbase-file-uploader__file-info", children: [jsx("div", { className: "designbase-file-uploader__file-icon", children: getStatusIcon(uploadFile.status) }), jsxs("div", { className: "designbase-file-uploader__file-details", children: [jsx("div", { className: "designbase-file-uploader__file-name", children: uploadFile.file.name }), jsx("div", { className: "designbase-file-uploader__file-size", children: formatFileSize(uploadFile.file.size) }), uploadFile.error && (jsx("div", { className: "designbase-file-uploader__file-error", children: uploadFile.error }))] })] }), showProgress && uploadFile.status === 'uploading' && (jsx("div", { className: "designbase-file-uploader__progress", children: jsx(Progressbar, { value: uploadFile.progress || 0, size: "s", variant: "primary", style: "solid", showLabel: true, labelPosition: "inside", fullWidth: true }) })), jsxs("div", { className: "designbase-file-uploader__file-actions", children: [uploadFile.status === 'error' && (jsx("button", { className: "designbase-file-uploader__retry-button", onClick: () => handleRetry(uploadFile.id), disabled: disabled || readonly, type: "button", children: "\uC7AC\uC2DC\uB3C4" })), jsx("button", { className: "designbase-file-uploader__remove-button", onClick: () => handleRemove(uploadFile.id), disabled: disabled || readonly, type: "button", children: "\uC0AD\uC81C" })] })] }, uploadFile.id))) })] }))] }));
6699
6752
  };
6700
6753
 
6701
6754
  const FloatingActionButton = forwardRef(({ size = 'm', variant = 'primary', icon, loading = false, disabled = false, onClick, className, style, ...props }, forwardedRef) => {
@@ -8357,6 +8410,229 @@ const Masonry = ({ images, columns = 3, spacing = 'm', ratio = 'auto', fit = 'co
8357
8410
  })), currentIndex: selectedImageIndex, isOpen: true, onOpenChange: handleLightboxClose, onImageChange: handleLightboxNavigate }))] }));
8358
8411
  };
8359
8412
 
8413
+ const OnboardingModal = ({ steps, currentStep = 0, isOpen, onClose, onStepChange, onComplete, showCloseButton = true, showIndicator = true, closeIcon: CloseIconComponent = CloseIcon, prevIcon: PrevIconComponent = ChevronLeftIcon, nextIcon: NextIconComponent = ChevronRightIcon, indicatorType = 'dots', indicatorSize = 'm', className, ...props }) => {
8414
+ const [internalStep, setInternalStep] = useState(currentStep);
8415
+ useEffect(() => {
8416
+ setInternalStep(currentStep);
8417
+ }, [currentStep]);
8418
+ const currentStepData = steps[internalStep];
8419
+ const isFirstStep = internalStep === 0;
8420
+ const isLastStep = internalStep === steps.length - 1;
8421
+ const handlePrev = () => {
8422
+ if (!isFirstStep) {
8423
+ const newStep = internalStep - 1;
8424
+ setInternalStep(newStep);
8425
+ onStepChange?.(newStep);
8426
+ }
8427
+ };
8428
+ const handleNext = () => {
8429
+ if (isLastStep) {
8430
+ onComplete?.();
8431
+ }
8432
+ else {
8433
+ const newStep = internalStep + 1;
8434
+ setInternalStep(newStep);
8435
+ onStepChange?.(newStep);
8436
+ }
8437
+ };
8438
+ const handleKeyDown = (e) => {
8439
+ if (e.key === 'Escape') {
8440
+ onClose();
8441
+ }
8442
+ else if (e.key === 'ArrowLeft' && !isFirstStep) {
8443
+ handlePrev();
8444
+ }
8445
+ else if (e.key === 'ArrowRight' && !isLastStep) {
8446
+ handleNext();
8447
+ }
8448
+ };
8449
+ if (!isOpen || !currentStepData) {
8450
+ return null;
8451
+ }
8452
+ const classes = clsx('designbase-onboarding-modal', className);
8453
+ return (jsxs("div", { className: classes, onKeyDown: handleKeyDown, tabIndex: -1, role: "dialog", "aria-modal": "true", "aria-labelledby": "onboarding-title", "aria-describedby": "onboarding-description", ...props, children: [jsx("div", { className: "designbase-onboarding-modal__overlay", onClick: onClose }), jsxs("div", { className: "designbase-onboarding-modal__content", children: [showCloseButton && (jsx("button", { type: "button", className: "designbase-onboarding-modal__close-button", onClick: onClose, "aria-label": "\uB2EB\uAE30", children: jsx(CloseIconComponent, { size: 20 }) })), jsxs("div", { className: "designbase-onboarding-modal__main", children: [currentStepData.image && (jsx("div", { className: "designbase-onboarding-modal__image", children: typeof currentStepData.image === 'string' ? (jsx("img", { src: currentStepData.image, alt: currentStepData.title || '온보딩 이미지' })) : (currentStepData.image) })), jsxs("div", { className: "designbase-onboarding-modal__text", children: [currentStepData.title && (jsx("h2", { id: "onboarding-title", className: "designbase-onboarding-modal__title", children: currentStepData.title })), currentStepData.description && (jsx("p", { id: "onboarding-description", className: "designbase-onboarding-modal__description", children: currentStepData.description })), currentStepData.content && (jsx("div", { className: "designbase-onboarding-modal__content-area", children: currentStepData.content }))] })] }), jsxs("div", { className: "designbase-onboarding-modal__navigation", children: [jsx(Button, { variant: "secondary", size: "m", startIcon: PrevIconComponent, onClick: handlePrev, disabled: isFirstStep, "aria-label": "\uC774\uC804", children: "\uC774\uC804" }), showIndicator && (jsx(Indicator, { current: internalStep, total: steps.length, type: indicatorType, size: indicatorSize, clickable: true, onStepClick: (step) => {
8454
+ setInternalStep(step);
8455
+ onStepChange?.(step);
8456
+ } })), jsx(Button, { variant: "primary", size: "m", endIcon: !isLastStep ? NextIconComponent : undefined, onClick: handleNext, "aria-label": isLastStep ? '완료' : '다음', children: isLastStep ? '완료' : '다음' })] })] })] }));
8457
+ };
8458
+ OnboardingModal.displayName = 'OnboardingModal';
8459
+
8460
+ const Tutorial = ({ steps, currentStep = 0, isActive, onStart, onEnd, onStepChange, onNext, onPrev, indicatorType = 'numbers', indicatorSize = 'm', closeIcon: CloseIconComponent = CloseIcon, prevIcon: PrevIconComponent = ChevronLeftIcon, nextIcon: NextIconComponent = ChevronRightIcon, className, ...props }) => {
8461
+ const [internalStep, setInternalStep] = useState(currentStep);
8462
+ const [targetElement, setTargetElement] = useState(null);
8463
+ const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 });
8464
+ const [popoverPlacement, setPopoverPlacement] = useState('bottom');
8465
+ const overlayRef = useRef(null);
8466
+ const popoverRef = useRef(null);
8467
+ const currentStepData = steps[internalStep];
8468
+ const isFirstStep = internalStep === 0;
8469
+ const isLastStep = internalStep === steps.length - 1;
8470
+ // 타겟 요소 찾기 및 위치 계산
8471
+ const updateTargetElement = useCallback(() => {
8472
+ if (!currentStepData?.target)
8473
+ return;
8474
+ // 이전 타겟 요소의 강조 스타일 제거
8475
+ if (targetElement) {
8476
+ targetElement.classList.remove('designbase-tutorial__target');
8477
+ targetElement.style.position = '';
8478
+ targetElement.style.zIndex = '';
8479
+ }
8480
+ const element = document.querySelector(currentStepData.target);
8481
+ if (!element)
8482
+ return;
8483
+ setTargetElement(element);
8484
+ // 타겟 요소에 강조 스타일 적용
8485
+ element.style.position = 'relative';
8486
+ element.style.zIndex = '1000';
8487
+ element.classList.add('designbase-tutorial__target');
8488
+ // 팝오버 위치 계산
8489
+ const rect = element.getBoundingClientRect();
8490
+ const viewportWidth = window.innerWidth;
8491
+ const viewportHeight = window.innerHeight;
8492
+ const popoverWidth = 300;
8493
+ const popoverHeight = 200;
8494
+ const margin = 16;
8495
+ let placement = 'bottom';
8496
+ let top = rect.bottom + margin;
8497
+ let left = rect.left + (rect.width / 2) - (popoverWidth / 2);
8498
+ // 자동 배치 로직
8499
+ if (currentStepData.placement === 'auto') {
8500
+ // 위쪽 공간 확인
8501
+ if (rect.top - popoverHeight - margin > 0) {
8502
+ placement = 'top';
8503
+ top = rect.top - popoverHeight - margin;
8504
+ }
8505
+ // 아래쪽 공간 확인
8506
+ else if (rect.bottom + popoverHeight + margin < viewportHeight) {
8507
+ placement = 'bottom';
8508
+ top = rect.bottom + margin;
8509
+ }
8510
+ // 왼쪽 공간 확인
8511
+ else if (rect.left - popoverWidth - margin > 0) {
8512
+ placement = 'left';
8513
+ top = rect.top + (rect.height / 2) - (popoverHeight / 2);
8514
+ left = rect.left - popoverWidth - margin;
8515
+ }
8516
+ // 오른쪽 공간 확인
8517
+ else if (rect.right + popoverWidth + margin < viewportWidth) {
8518
+ placement = 'right';
8519
+ top = rect.top + (rect.height / 2) - (popoverHeight / 2);
8520
+ left = rect.right + margin;
8521
+ }
8522
+ }
8523
+ else {
8524
+ placement = currentStepData.placement || 'bottom';
8525
+ switch (placement) {
8526
+ case 'top':
8527
+ top = rect.top - popoverHeight - margin;
8528
+ break;
8529
+ case 'bottom':
8530
+ top = rect.bottom + margin;
8531
+ break;
8532
+ case 'left':
8533
+ top = rect.top + (rect.height / 2) - (popoverHeight / 2);
8534
+ left = rect.left - popoverWidth - margin;
8535
+ break;
8536
+ case 'right':
8537
+ top = rect.top + (rect.height / 2) - (popoverHeight / 2);
8538
+ left = rect.right + margin;
8539
+ break;
8540
+ }
8541
+ }
8542
+ // 뷰포트 경계 확인 및 조정
8543
+ left = Math.max(margin, Math.min(left, viewportWidth - popoverWidth - margin));
8544
+ top = Math.max(margin, Math.min(top, viewportHeight - popoverHeight - margin));
8545
+ setPopoverPosition({ top, left });
8546
+ setPopoverPlacement(placement);
8547
+ }, [currentStepData]);
8548
+ // 단계 변경 시 타겟 업데이트
8549
+ useEffect(() => {
8550
+ if (isActive && currentStepData) {
8551
+ updateTargetElement();
8552
+ }
8553
+ }, [internalStep, isActive, updateTargetElement]);
8554
+ // 튜토리얼 비활성화 시 모든 강조 스타일 제거
8555
+ useEffect(() => {
8556
+ if (!isActive && targetElement) {
8557
+ targetElement.classList.remove('designbase-tutorial__target');
8558
+ targetElement.style.position = '';
8559
+ targetElement.style.zIndex = '';
8560
+ setTargetElement(null);
8561
+ }
8562
+ }, [isActive, targetElement]);
8563
+ // 윈도우 리사이즈 시 위치 재계산
8564
+ useEffect(() => {
8565
+ if (!isActive)
8566
+ return;
8567
+ const handleResize = () => {
8568
+ updateTargetElement();
8569
+ };
8570
+ window.addEventListener('resize', handleResize);
8571
+ return () => window.removeEventListener('resize', handleResize);
8572
+ }, [isActive, updateTargetElement]);
8573
+ const handleNext = () => {
8574
+ if (isLastStep) {
8575
+ handleEnd();
8576
+ }
8577
+ else {
8578
+ // 이전 타겟 요소의 강조 스타일 제거
8579
+ if (targetElement) {
8580
+ targetElement.classList.remove('designbase-tutorial__target');
8581
+ targetElement.style.position = '';
8582
+ targetElement.style.zIndex = '';
8583
+ }
8584
+ const newStep = internalStep + 1;
8585
+ setInternalStep(newStep);
8586
+ onStepChange?.(newStep);
8587
+ onNext?.();
8588
+ }
8589
+ };
8590
+ const handlePrev = () => {
8591
+ if (!isFirstStep) {
8592
+ // 이전 타겟 요소의 강조 스타일 제거
8593
+ if (targetElement) {
8594
+ targetElement.classList.remove('designbase-tutorial__target');
8595
+ targetElement.style.position = '';
8596
+ targetElement.style.zIndex = '';
8597
+ }
8598
+ const newStep = internalStep - 1;
8599
+ setInternalStep(newStep);
8600
+ onStepChange?.(newStep);
8601
+ onPrev?.();
8602
+ }
8603
+ };
8604
+ const handleEnd = () => {
8605
+ // 타겟 요소에서 강조 스타일 제거
8606
+ if (targetElement) {
8607
+ targetElement.classList.remove('designbase-tutorial__target');
8608
+ targetElement.style.position = '';
8609
+ targetElement.style.zIndex = '';
8610
+ }
8611
+ setTargetElement(null);
8612
+ onEnd?.();
8613
+ };
8614
+ const handleKeyDown = (e) => {
8615
+ if (e.key === 'Escape') {
8616
+ handleEnd();
8617
+ }
8618
+ else if (e.key === 'ArrowLeft' && !isFirstStep) {
8619
+ handlePrev();
8620
+ }
8621
+ else if (e.key === 'ArrowRight' && !isLastStep) {
8622
+ handleNext();
8623
+ }
8624
+ };
8625
+ if (!isActive || !currentStepData) {
8626
+ return null;
8627
+ }
8628
+ const classes = clsx('designbase-tutorial', className);
8629
+ return (jsxs("div", { className: classes, onKeyDown: handleKeyDown, tabIndex: -1, role: "dialog", "aria-modal": "true", "aria-labelledby": "tutorial-title", "aria-describedby": "tutorial-content", ...props, children: [jsx("div", { ref: overlayRef, className: "designbase-tutorial__overlay", onClick: handleEnd }), jsxs("div", { ref: popoverRef, className: clsx('designbase-tutorial__popover', `designbase-tutorial__popover--${popoverPlacement}`), style: {
8630
+ top: popoverPosition.top,
8631
+ left: popoverPosition.left
8632
+ }, children: [jsx("button", { type: "button", className: "designbase-tutorial__close-button", onClick: handleEnd, "aria-label": "\uD29C\uD1A0\uB9AC\uC5BC \uC885\uB8CC", children: jsx(CloseIconComponent, { size: 16 }) }), jsxs("div", { className: "designbase-tutorial__content", children: [(currentStepData.title || currentStepData.content || currentStepData.children) && (jsxs("div", { className: "designbase-tutorial__text", children: [currentStepData.title && (jsx("h3", { id: "tutorial-title", className: "designbase-tutorial__title", children: currentStepData.title })), currentStepData.content && (jsx("p", { id: "tutorial-content", className: "designbase-tutorial__description", children: currentStepData.content })), currentStepData.children && (jsx("div", { className: "designbase-tutorial__children", children: currentStepData.children }))] })), jsxs("div", { className: "designbase-tutorial__navigation", children: [!isFirstStep && (jsx(Button, { variant: "secondary", size: "s", startIcon: jsx(PrevIconComponent, { size: 16 }), onPress: handlePrev, "aria-label": "\uC774\uC804 \uB2E8\uACC4", children: "\uC774\uC804" })), jsx(Indicator, { current: internalStep, total: steps.length, type: indicatorType, size: indicatorSize }), jsx(Button, { variant: "primary", size: "s", endIcon: !isLastStep ? jsx(NextIconComponent, { size: 16 }) : undefined, onPress: handleNext, "aria-label": isLastStep ? '완료' : '다음 단계', children: isLastStep ? '완료' : '다음' })] })] })] })] }));
8633
+ };
8634
+ Tutorial.displayName = 'Tutorial';
8635
+
8360
8636
  const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size = 'm', variant = 'default', disabled = false, readOnly = false, fullWidth = false, searchIcon: SearchIconComponent = SearchIcon, clearIcon: ClearIconComponent = CloseIcon, enableRecentSearches = false, recentSearchesKey = 'searchbar-recent-searches', suggestedSearches = [], suggestionRollingInterval = 5000, onChange, onSearch, onFocus, onBlur, onKeyDown, className, ...props }) => {
8361
8637
  const [internalValue, setInternalValue] = useState(defaultValue);
8362
8638
  const [recentSearches, setRecentSearches] = useState([]);
@@ -8380,9 +8656,9 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8380
8656
  }
8381
8657
  }
8382
8658
  }, [enableRecentSearches, recentSearchesKey]);
8383
- // 추천 검색어 롤링 (포커스 없이도 계속)
8659
+ // 추천 검색어 롤링 (value가 없을 때만)
8384
8660
  useEffect(() => {
8385
- if (suggestedSearches.length > 0 && !currentValue) {
8661
+ if (suggestedSearches.length > 0 && !currentValue && currentValue === '') {
8386
8662
  suggestionIntervalRef.current = setInterval(() => {
8387
8663
  setCurrentSuggestion(prev => (prev + 1) % suggestedSearches.length);
8388
8664
  }, suggestionRollingInterval);
@@ -8481,6 +8757,11 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8481
8757
  const handleSuggestionClick = (suggestion) => {
8482
8758
  setInternalValue(suggestion);
8483
8759
  onChange?.(suggestion);
8760
+ // 롤링 중단
8761
+ if (suggestionIntervalRef.current) {
8762
+ clearInterval(suggestionIntervalRef.current);
8763
+ suggestionIntervalRef.current = null;
8764
+ }
8484
8765
  handleSearch(suggestion);
8485
8766
  };
8486
8767
  const classes = clsx('designbase-search-bar', `designbase-search-bar--${size}`, `designbase-search-bar--${variant}`, {
@@ -8493,8 +8774,8 @@ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size =
8493
8774
  'designbase-search-bar__input--disabled': disabled,
8494
8775
  'designbase-search-bar__input--readonly': readOnly,
8495
8776
  });
8496
- // 현재 플레이스홀더 (추천 검색어가 있으면 롤링)
8497
- const currentPlaceholder = suggestedSearches.length > 0 && !currentValue
8777
+ // 현재 플레이스홀더 (value가 없을 때만 추천 검색어 롤링)
8778
+ const currentPlaceholder = suggestedSearches.length > 0 && !currentValue && currentValue === ''
8498
8779
  ? suggestedSearches[currentSuggestion]
8499
8780
  : placeholder;
8500
8781
  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', {
@@ -11370,5 +11651,5 @@ const toggleTheme = () => {
11370
11651
  console.log('toggleTheme called');
11371
11652
  };
11372
11653
 
11373
- export { Accordion, AdBanner, Alert, AnimationBackground, AnimationText, AudioPlayer, Avatar, Backdrop, Badge, Banner, BottomSheet, Breadcrumbs, Button, Calendar, Card, Carousel, Checkbox, Chip, ColorPicker, Confirm, Container, ContextMenu, Countdown, DatePicker, Divider, Drawer, Dropdown, Dropzone, EmptyState, FileUploader, FloatingActionButton, Form, Gradient, Grid, HeroFeature, Image$1 as Image, ImageList, Input, Label, Lightbox, List, Logo, MarkdownEditor, Masonry, MenuItem, Modal, ModalBody, ModalFooter, ModalHeader, Navbar, Pagination, Popover, Progress, ProgressStep, Progressbar, Radio, RandomGradient, RangeSlider, Rating, Reorder, ResizablePanels, ScrollArea, SearchBar, Section, SegmentControl, Select, Share, Sidebar, Skeleton, Spinner, SplitView, Stack, Stat, Stepper, Table, Tabs, Textarea, TimePicker, Timeline, Toast, Toggle, Toolbar, Tooltip, VideoPlayer, YouTubePlayer, getTheme, setTheme, toggleTheme };
11654
+ export { Accordion, AdBanner, Alert, AnimationBackground, AnimationText, AudioPlayer, Avatar, Backdrop, Badge, Banner, BottomSheet, Breadcrumbs, Button, Calendar, Card, Carousel, Checkbox, Chip, ColorPicker, Confirm, Container, ContextMenu, Countdown, DatePicker, Divider, Drawer, Dropdown, Dropzone, EmptyState, FileUploader, FloatingActionButton, Form, Gradient, Grid, HeroFeature, Image$1 as Image, ImageList, Indicator, Input, Label, Lightbox, List, Logo, MarkdownEditor, Masonry, MenuItem, Modal, ModalBody, ModalFooter, ModalHeader, Navbar, OnboardingModal, Pagination, Popover, Progress, ProgressStep, Progressbar, Radio, RandomGradient, RangeSlider, Rating, Reorder, ResizablePanels, ScrollArea, SearchBar, Section, SegmentControl, Select, Share, Sidebar, Skeleton, Spinner, SplitView, Stack, Stat, Stepper, Table, Tabs, Textarea, TimePicker, Timeline, Toast, Toggle, Toolbar, Tooltip, Tutorial, VideoPlayer, YouTubePlayer, getTheme, setTheme, toggleTheme };
11374
11655
  //# sourceMappingURL=index.esm.js.map