@designbasekorea/ui 0.2.41 → 0.3.1

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.umd.js CHANGED
@@ -2417,23 +2417,18 @@
2417
2417
  return `designbase-button--radius-${radius}`;
2418
2418
  }
2419
2419
  // 아이콘 전용 버튼도 일반 버튼과 동일한 radius 적용
2420
- return `designbase-button--radius-${size === 'xs' || size === 's' ? 's' : size === 'l' || size === 'xl' ? 'l' : 'm'}`;
2420
+ if (size === 's')
2421
+ return 'designbase-button--radius-s';
2422
+ if (size === 'l')
2423
+ return 'designbase-button--radius-l';
2424
+ return 'designbase-button--radius-m';
2421
2425
  };
2422
2426
  const classes = clsx('designbase-button', `designbase-button--${variant}`, `designbase-button--${size}`, getRadiusClass(), {
2423
2427
  'designbase-button--full-width': fullWidth,
2424
2428
  'designbase-button--loading': loading,
2425
2429
  'designbase-button--icon-only': iconOnly,
2426
2430
  }, className);
2427
- const iconSize = (() => {
2428
- switch (size) {
2429
- case 'xs': return 12;
2430
- case 's': return 14;
2431
- case 'm': return 16;
2432
- case 'l': return 18;
2433
- case 'xl': return 20;
2434
- default: return 16;
2435
- }
2436
- })();
2431
+ const iconSize = size === 's' ? 14 : size === 'l' ? 18 : 16;
2437
2432
  // 아이콘 색상 결정
2438
2433
  const getIconColor = () => {
2439
2434
  switch (variant) {
@@ -2453,7 +2448,7 @@
2453
2448
  };
2454
2449
  const renderContent = () => {
2455
2450
  if (loading) {
2456
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Spinner, { type: "circular", size: size === 'xs' ? 'xs' : size === 's' ? 's' : 'm', color: getIconColor(), speed: 1, showLabel: false }), !iconOnly && jsxRuntime.jsx("span", { children: "\uB85C\uB529 \uC911..." })] }));
2451
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Spinner, { type: "circular", size: size === 's' ? 's' : size === 'l' ? 'l' : 'm', color: getIconColor(), speed: 1, showLabel: false }), !iconOnly && jsxRuntime.jsx("span", { children: "\uB85C\uB529 \uC911..." })] }));
2457
2452
  }
2458
2453
  // iconOnly 버튼일 때는 children을 아이콘으로 처리
2459
2454
  if (iconOnly && children && React.isValidElement(children)) {
@@ -4012,7 +4007,7 @@
4012
4007
  ModalBody.displayName = 'ModalBody';
4013
4008
  ModalFooter.displayName = 'ModalFooter';
4014
4009
 
4015
- const Label = ({ children, htmlFor, required = false, size = 's', disabled = false, error = false, className, onClick, }) => {
4010
+ const Label = ({ children, htmlFor, id, required = false, size = 's', disabled = false, error = false, className, onClick, }) => {
4016
4011
  const classes = [
4017
4012
  'designbase-label',
4018
4013
  `designbase-label--${size}`,
@@ -4022,7 +4017,7 @@
4022
4017
  ]
4023
4018
  .filter(Boolean)
4024
4019
  .join(' ');
4025
- return (jsxRuntime.jsxs("label", { htmlFor: htmlFor, className: classes, onClick: onClick, children: [children, required && jsxRuntime.jsx("span", { className: "designbase-label__required", children: "*" })] }));
4020
+ return (jsxRuntime.jsxs("label", { htmlFor: htmlFor, id: id, className: classes, onClick: onClick, children: [children, required && jsxRuntime.jsx("span", { className: "designbase-label__required", children: "*" })] }));
4026
4021
  };
4027
4022
  Label.displayName = 'Label';
4028
4023
 
@@ -4053,12 +4048,17 @@
4053
4048
  'designbase-input--with-end-icon': EndIcon || isPassword,
4054
4049
  }, className);
4055
4050
  const inputClasses = clsx('designbase-input__field', inputClassName);
4056
- const iconSizeMap = {
4057
- sm: 14,
4058
- md: 16,
4059
- lg: 18,
4060
- };
4061
- const iconSize = iconSizeMap[size];
4051
+ const iconSize = React.useMemo(() => {
4052
+ switch (size) {
4053
+ case 's':
4054
+ return 16;
4055
+ case 'l':
4056
+ return 24;
4057
+ case 'm':
4058
+ default:
4059
+ return 20;
4060
+ }
4061
+ }, [size]);
4062
4062
  return (jsxRuntime.jsxs("div", { className: classes, children: [label && (jsxRuntime.jsx(Label, { htmlFor: inputId, required: required, error: error, disabled: disabled, size: size === 's' ? 's' : size === 'm' ? 'm' : 'l', children: label })), jsxRuntime.jsxs("div", { className: "designbase-input__wrapper", children: [actualStartIcon && (jsxRuntime.jsx("div", { className: "designbase-input__start-icon", children: React.createElement(actualStartIcon, { size: iconSize }) })), jsxRuntime.jsx("input", { ...props, ref: forwardedRef, id: inputId, type: actualType, value: value, defaultValue: defaultValue, placeholder: placeholder, disabled: disabled, readOnly: readOnly, required: required, className: inputClasses, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, "aria-describedby": clsx(helperText && helperTextId, error && errorMessage && errorMessageId), "aria-invalid": error }), isPassword && (jsxRuntime.jsx("button", { type: "button", className: "designbase-input__password-toggle", onClick: handlePasswordToggle, disabled: disabled, "aria-label": showPassword ? '비밀번호 숨기기' : '비밀번호 보기', children: React.createElement(PasswordIcon, { size: iconSize }) })), EndIcon && !isPassword && (jsxRuntime.jsx("div", { className: "designbase-input__end-icon", children: React.createElement(EndIcon, { size: iconSize }) }))] }), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "designbase-input__helper-text", children: helperText })), error && errorMessage && (jsxRuntime.jsx("p", { id: errorMessageId, className: "designbase-input__error-message", children: errorMessage }))] }));
4063
4063
  });
4064
4064
  Input.displayName = 'Input';
@@ -4099,6 +4099,157 @@
4099
4099
  });
4100
4100
  Checkbox.displayName = 'Checkbox';
4101
4101
 
4102
+ const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size = 'm', variant = 'default', disabled = false, readOnly = false, fullWidth = false, searchIcon: SearchIconComponent = icons.SearchIcon, clearIcon: ClearIconComponent = icons.CloseIcon, enableRecentSearches = false, recentSearchesKey = 'searchbar-recent-searches', suggestedSearches = [], suggestionRollingInterval = 5000, onChange, onSearch, onFocus, onBlur, onKeyDown, className, ...rest }) => {
4103
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
4104
+ const [recentSearches, setRecentSearches] = React.useState([]);
4105
+ const [showRecentSearches, setShowRecentSearches] = React.useState(false);
4106
+ const [currentSuggestion, setCurrentSuggestion] = React.useState(0);
4107
+ const [isFocused, setIsFocused] = React.useState(false);
4108
+ const inputRef = React.useRef(null);
4109
+ const suggestionIntervalRef = React.useRef(null);
4110
+ const currentValue = value !== undefined ? value : internalValue;
4111
+ // 최근 검색어 로드
4112
+ React.useEffect(() => {
4113
+ if (enableRecentSearches) {
4114
+ try {
4115
+ const stored = localStorage.getItem(recentSearchesKey);
4116
+ if (stored) {
4117
+ setRecentSearches(JSON.parse(stored));
4118
+ }
4119
+ }
4120
+ catch (error) {
4121
+ console.warn('최근 검색어를 불러올 수 없습니다:', error);
4122
+ }
4123
+ }
4124
+ }, [enableRecentSearches, recentSearchesKey]);
4125
+ // 추천 검색어 롤링 (value가 없을 때만)
4126
+ React.useEffect(() => {
4127
+ if (suggestedSearches.length > 0 && !currentValue && currentValue === '') {
4128
+ suggestionIntervalRef.current = setInterval(() => {
4129
+ setCurrentSuggestion(prev => (prev + 1) % suggestedSearches.length);
4130
+ }, suggestionRollingInterval);
4131
+ }
4132
+ else {
4133
+ if (suggestionIntervalRef.current) {
4134
+ clearInterval(suggestionIntervalRef.current);
4135
+ suggestionIntervalRef.current = null;
4136
+ }
4137
+ }
4138
+ return () => {
4139
+ if (suggestionIntervalRef.current) {
4140
+ clearInterval(suggestionIntervalRef.current);
4141
+ }
4142
+ };
4143
+ }, [suggestedSearches.length, currentValue, suggestionRollingInterval]);
4144
+ React.useEffect(() => {
4145
+ if (value !== undefined) {
4146
+ setInternalValue(value);
4147
+ }
4148
+ }, [value]);
4149
+ const handleChange = (e) => {
4150
+ const newValue = e.target.value;
4151
+ setInternalValue(newValue);
4152
+ onChange?.(newValue);
4153
+ };
4154
+ const handleClear = () => {
4155
+ setInternalValue('');
4156
+ onChange?.('');
4157
+ inputRef.current?.focus();
4158
+ };
4159
+ const handleSearch = (searchValue) => {
4160
+ const searchTerm = searchValue || currentValue.trim();
4161
+ if (searchTerm) {
4162
+ onSearch?.(searchTerm);
4163
+ // 최근 검색어 저장
4164
+ if (enableRecentSearches) {
4165
+ const newRecentSearches = [
4166
+ searchTerm,
4167
+ ...recentSearches.filter(item => item !== searchTerm)
4168
+ ].slice(0, 10); // 최대 10개
4169
+ setRecentSearches(newRecentSearches);
4170
+ try {
4171
+ localStorage.setItem(recentSearchesKey, JSON.stringify(newRecentSearches));
4172
+ }
4173
+ catch (error) {
4174
+ console.warn('최근 검색어를 저장할 수 없습니다:', error);
4175
+ }
4176
+ }
4177
+ }
4178
+ };
4179
+ const handleKeyDown = (e) => {
4180
+ if (e.key === 'Enter') {
4181
+ handleSearch();
4182
+ }
4183
+ onKeyDown?.(e);
4184
+ };
4185
+ const handleFocus = (e) => {
4186
+ setIsFocused(true);
4187
+ setShowRecentSearches(enableRecentSearches && recentSearches.length > 0);
4188
+ onFocus?.(e);
4189
+ };
4190
+ const handleBlur = (e) => {
4191
+ setIsFocused(false);
4192
+ // 약간의 지연을 두어 클릭 이벤트가 처리되도록 함
4193
+ setTimeout(() => {
4194
+ setShowRecentSearches(false);
4195
+ }, 200);
4196
+ onBlur?.(e);
4197
+ };
4198
+ const handleRecentSearchClick = (searchTerm) => {
4199
+ setInternalValue(searchTerm);
4200
+ onChange?.(searchTerm);
4201
+ setShowRecentSearches(false);
4202
+ handleSearch(searchTerm);
4203
+ };
4204
+ const handleRemoveRecentSearch = (searchTerm) => {
4205
+ const newRecentSearches = recentSearches.filter(item => item !== searchTerm);
4206
+ setRecentSearches(newRecentSearches);
4207
+ try {
4208
+ localStorage.setItem(recentSearchesKey, JSON.stringify(newRecentSearches));
4209
+ }
4210
+ catch (error) {
4211
+ console.warn('최근 검색어를 삭제할 수 없습니다:', error);
4212
+ }
4213
+ };
4214
+ const handleClearAllRecentSearches = () => {
4215
+ setRecentSearches([]);
4216
+ try {
4217
+ localStorage.removeItem(recentSearchesKey);
4218
+ }
4219
+ catch (error) {
4220
+ console.warn('최근 검색어를 모두 삭제할 수 없습니다:', error);
4221
+ }
4222
+ };
4223
+ const handleSuggestionClick = (suggestion) => {
4224
+ setInternalValue(suggestion);
4225
+ onChange?.(suggestion);
4226
+ // 롤링 중단
4227
+ if (suggestionIntervalRef.current) {
4228
+ clearInterval(suggestionIntervalRef.current);
4229
+ suggestionIntervalRef.current = null;
4230
+ }
4231
+ handleSearch(suggestion);
4232
+ };
4233
+ const classes = clsx('designbase-search-bar', `designbase-search-bar--${size}`, `designbase-search-bar--${variant}`, {
4234
+ 'designbase-search-bar--disabled': disabled,
4235
+ 'designbase-search-bar--readonly': readOnly,
4236
+ 'designbase-search-bar--full-width': fullWidth,
4237
+ 'designbase-search-bar--has-value': currentValue && currentValue.length > 0,
4238
+ }, className);
4239
+ const inputClasses = clsx('designbase-search-bar__input', {
4240
+ 'designbase-search-bar__input--disabled': disabled,
4241
+ 'designbase-search-bar__input--readonly': readOnly,
4242
+ });
4243
+ // 현재 플레이스홀더 (value가 없을 때만 추천 검색어 롤링)
4244
+ const currentPlaceholder = suggestedSearches.length > 0 && !currentValue && currentValue === ''
4245
+ ? suggestedSearches[currentSuggestion]
4246
+ : placeholder;
4247
+ 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", ...rest }), 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', {
4248
+ 'designbase-search-bar__suggestion-item--active': index === currentSuggestion
4249
+ }), onClick: () => handleSuggestionClick(suggestion), children: suggestion }, index))) })] }))] }));
4250
+ };
4251
+ SearchBar.displayName = 'SearchBar';
4252
+
4102
4253
  const Select = ({ value, defaultValue, options, label, placeholder = '선택하세요', multiple = false, searchable = false, disabled = false, readOnly = false, required = false, error = false, errorMessage, helperText, size = 'm', fullWidth = false, dropdownWidth = 'auto', position = 'bottom', maxHeight = 200, showClearButton = true, className, onChange, onFocus, onBlur, ...props }) => {
4103
4254
  const [isOpen, setIsOpen] = React.useState(false);
4104
4255
  const [selectedValue, setSelectedValue] = React.useState(value ?? defaultValue ?? (multiple ? [] : ''));
@@ -4243,9 +4394,6 @@
4243
4394
  setSelectedValue(newValue);
4244
4395
  onChange?.(newValue);
4245
4396
  };
4246
- const handleSearchChange = (e) => {
4247
- setSearchTerm(e.target.value);
4248
- };
4249
4397
  const classes = clsx('designbase-select', `designbase-select--${size}`, {
4250
4398
  'designbase-select--open': isOpen,
4251
4399
  'designbase-select--error': error,
@@ -4253,9 +4401,11 @@
4253
4401
  'designbase-select--readonly': readOnly,
4254
4402
  'designbase-select--full-width': fullWidth,
4255
4403
  'designbase-select--multiple': multiple,
4404
+ 'designbase-select--searchable': searchable,
4256
4405
  }, className);
4257
4406
  const triggerClasses = clsx('designbase-select__trigger', {
4258
4407
  'designbase-select__trigger--focused': isOpen,
4408
+ 'designbase-select__trigger--searchable': searchable,
4259
4409
  });
4260
4410
  const dropdownClasses = clsx('designbase-select__dropdown', `designbase-select__dropdown--${dropdownWidth}`, `designbase-select__dropdown--${position}`, {
4261
4411
  'designbase-select__dropdown--open': isOpen,
@@ -4263,23 +4413,23 @@
4263
4413
  const filteredOptions = getFilteredOptions();
4264
4414
  const selectedLabels = getSelectedLabels();
4265
4415
  const hasValue = multiple ? selectedValue.length > 0 : selectedValue !== '';
4266
- return (jsxRuntime.jsxs("div", { className: classes, ref: containerRef, children: [label && (jsxRuntime.jsxs("label", { className: "designbase-select__label", children: [label, required && jsxRuntime.jsx("span", { className: "designbase-select__required", children: "*" })] })), jsxRuntime.jsxs("div", { className: triggerClasses, onClick: handleToggle, onFocus: onFocus, onBlur: onBlur, tabIndex: disabled || readOnly ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-labelledby": label ? 'select-label' : undefined, ...props, children: [jsxRuntime.jsx("div", { className: "designbase-select__value", children: multiple ? (jsxRuntime.jsxs("div", { className: "designbase-select__tags", ref: tagsContainerRef, children: [selectedValue.map((value) => {
4267
- const option = options.find(opt => opt.value === value);
4268
- return (jsxRuntime.jsxs("span", { className: "designbase-select__tag", children: [jsxRuntime.jsx("span", { className: "designbase-select__tag-label", children: option?.label || value }), jsxRuntime.jsx("button", { type: "button", className: "designbase-select__tag-remove", onClick: (e) => {
4269
- e.stopPropagation();
4270
- handleRemoveValue(value);
4271
- }, children: jsxRuntime.jsx(icons.CloseIcon, { size: 12 }) })] }, value));
4272
- }), searchable && isOpen && (jsxRuntime.jsx("input", { ref: inputRef, type: "text", className: "designbase-select__search-input", value: searchTerm, onChange: handleSearchChange, placeholder: "\uAC80\uC0C9...", onClick: (e) => e.stopPropagation() }))] })) : (jsxRuntime.jsx("span", { className: "designbase-select__single-value", children: selectedLabels || placeholder })) }), jsxRuntime.jsxs("div", { className: "designbase-select__indicators", children: [showClearButton && hasValue && !disabled && !readOnly && (jsxRuntime.jsx("button", { type: "button", className: "designbase-select__clear-button", onClick: handleClearAll, "aria-label": "\uBAA8\uB4E0 \uAC12 \uC9C0\uC6B0\uAE30", children: jsxRuntime.jsx(icons.CloseIcon, { size: 16 }) })), jsxRuntime.jsx("div", { className: "designbase-select__chevron", children: isOpen ? jsxRuntime.jsx(icons.ChevronUpIcon, { size: 16 }) : jsxRuntime.jsx(icons.ChevronDownIcon, { size: 16 }) })] })] }), jsxRuntime.jsx("div", { className: dropdownClasses, ref: dropdownRef, children: jsxRuntime.jsx("div", { className: "designbase-select__options", style: { maxHeight: `${maxHeight}px` }, role: "listbox", children: filteredOptions.length === 0 ? (jsxRuntime.jsx("div", { className: "designbase-select__no-options", children: searchTerm ? '검색 결과가 없습니다.' : '옵션이 없습니다.' })) : (filteredOptions.map((option, index) => {
4273
- const isSelected = multiple
4274
- ? selectedValue.includes(option.value)
4275
- : selectedValue === option.value;
4276
- const isFocused = index === focusedIndex;
4277
- return (jsxRuntime.jsxs("div", { className: clsx('designbase-select__option', {
4278
- 'designbase-select__option--selected': isSelected,
4279
- 'designbase-select__option--focused': isFocused,
4280
- 'designbase-select__option--disabled': option.disabled,
4281
- }), onClick: () => handleOptionSelect(option), role: "option", "aria-selected": isSelected, children: [multiple && (jsxRuntime.jsx("div", { className: "designbase-select__checkbox", children: jsxRuntime.jsx(Checkbox, { isSelected: isSelected, isDisabled: option.disabled, size: "s", hasLabel: false, onChange: () => handleOptionSelect(option) }) })), jsxRuntime.jsx("span", { className: "designbase-select__option-label", children: option.label })] }, option.value));
4282
- })) }) }), helperText && !error && (jsxRuntime.jsx("p", { className: "designbase-select__helper-text", children: helperText })), error && errorMessage && (jsxRuntime.jsx("p", { className: "designbase-select__error-message", children: errorMessage }))] }));
4416
+ return (jsxRuntime.jsxs("div", { className: classes, ref: containerRef, children: [label && (jsxRuntime.jsxs("label", { className: "designbase-select__label", children: [label, required && jsxRuntime.jsx("span", { className: "designbase-select__required", children: "*" })] })), jsxRuntime.jsxs("div", { className: triggerClasses, onClick: handleToggle, onFocus: onFocus, onBlur: onBlur, tabIndex: disabled || readOnly ? -1 : 0, role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-labelledby": label ? 'select-label' : undefined, ...props, children: [jsxRuntime.jsx("div", { className: "designbase-select__value", children: multiple ? (jsxRuntime.jsx("div", { className: "designbase-select__tags", ref: tagsContainerRef, children: selectedValue.map((value) => {
4417
+ const option = options.find(opt => opt.value === value);
4418
+ return (jsxRuntime.jsxs("span", { className: "designbase-select__tag", children: [jsxRuntime.jsx("span", { className: "designbase-select__tag-label", children: option?.label || value }), jsxRuntime.jsx("button", { type: "button", className: "designbase-select__tag-remove", onClick: (e) => {
4419
+ e.stopPropagation();
4420
+ handleRemoveValue(value);
4421
+ }, children: jsxRuntime.jsx(icons.CloseIcon, { size: 12 }) })] }, value));
4422
+ }) })) : (jsxRuntime.jsx("span", { className: "designbase-select__single-value", children: selectedLabels || placeholder })) }), jsxRuntime.jsxs("div", { className: "designbase-select__indicators", children: [showClearButton && hasValue && !disabled && !readOnly && (jsxRuntime.jsx("button", { type: "button", className: "designbase-select__clear-button", onClick: handleClearAll, "aria-label": "\uBAA8\uB4E0 \uAC12 \uC9C0\uC6B0\uAE30", children: jsxRuntime.jsx(icons.CloseIcon, { size: 16 }) })), jsxRuntime.jsx("div", { className: "designbase-select__chevron", children: isOpen ? jsxRuntime.jsx(icons.ChevronUpIcon, { size: 16 }) : jsxRuntime.jsx(icons.ChevronDownIcon, { size: 16 }) })] })] }), jsxRuntime.jsxs("div", { className: dropdownClasses, ref: dropdownRef, children: [searchable && (jsxRuntime.jsx("div", { className: "designbase-select__search", onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: jsxRuntime.jsx(SearchBar, { value: searchTerm, onChange: (value) => setSearchTerm(value), onSearch: (val) => setSearchTerm(val), placeholder: "\uC635\uC158 \uAC80\uC0C9", size: size, variant: "outlined", fullWidth: true, onFocus: (e) => e.stopPropagation(), onBlur: (e) => e.stopPropagation() }) })), jsxRuntime.jsx("div", { className: "designbase-select__options", style: { maxHeight: `${maxHeight}px` }, role: "listbox", children: filteredOptions.length === 0 ? (jsxRuntime.jsx("div", { className: "designbase-select__no-options", children: searchTerm ? '검색 결과가 없습니다.' : '옵션이 없습니다.' })) : (filteredOptions.map((option, index) => {
4423
+ const isSelected = multiple
4424
+ ? selectedValue.includes(option.value)
4425
+ : selectedValue === option.value;
4426
+ const isFocused = index === focusedIndex;
4427
+ return (jsxRuntime.jsxs("div", { className: clsx('designbase-select__option', {
4428
+ 'designbase-select__option--selected': isSelected,
4429
+ 'designbase-select__option--focused': isFocused,
4430
+ 'designbase-select__option--disabled': option.disabled,
4431
+ }), onClick: () => handleOptionSelect(option), role: "option", "aria-selected": isSelected, children: [multiple && (jsxRuntime.jsx("div", { className: "designbase-select__checkbox", children: jsxRuntime.jsx(Checkbox, { isSelected: isSelected, isDisabled: option.disabled, size: "s", hasLabel: false, onChange: () => handleOptionSelect(option) }) })), jsxRuntime.jsx("span", { className: "designbase-select__option-label", children: option.label })] }, option.value));
4432
+ })) })] }), helperText && !error && (jsxRuntime.jsx("p", { className: "designbase-select__helper-text", children: helperText })), error && errorMessage && (jsxRuntime.jsx("p", { className: "designbase-select__error-message", children: errorMessage }))] }));
4283
4433
  };
4284
4434
  Select.displayName = 'Select';
4285
4435
 
@@ -5220,8 +5370,6 @@
5220
5370
  const handleTouchEndEvent = React.useCallback(() => {
5221
5371
  handleTouchEnd();
5222
5372
  }, [handleTouchEnd]);
5223
- // 현재 아이템
5224
- const currentItem = items[currentIndex];
5225
5373
  // 슬라이드 스타일 계산
5226
5374
  const getSlideStyle = () => {
5227
5375
  const containerWidth = containerRef.current?.offsetWidth || 1;
@@ -5236,9 +5384,6 @@
5236
5384
  // 네비게이션 버튼 비활성화 상태
5237
5385
  const isPrevDisabled = !infinite && currentIndex === 0;
5238
5386
  const isNextDisabled = !infinite && currentIndex === items.length - 1;
5239
- // 좋아요/북마크 상태
5240
- const isLiked = currentItem ? likedItems.has(currentItem.id) : false;
5241
- const isBookmarked = currentItem ? bookmarkedItems.has(currentItem.id) : false;
5242
5387
  if (items.length === 0)
5243
5388
  return null;
5244
5389
  return (jsxRuntime.jsxs("div", { className: clsx('designbase-carousel', `designbase-carousel--size-${size}`, `designbase-carousel--variant-${variant}`, `designbase-carousel--theme-${theme}`, `designbase-carousel--transition-${transition}`, `designbase-carousel--indicator-${indicatorStyle}`, {
@@ -5259,13 +5404,17 @@
5259
5404
  color: item.textColor || (item.image ? '#ffffff' : undefined),
5260
5405
  }, children: item.title })), showDescription && item.description && (jsxRuntime.jsx("p", { className: "designbase-carousel__slide-description", style: {
5261
5406
  color: item.textColor || (item.image ? '#ffffff' : undefined),
5262
- }, children: item.description })), item.meta && (jsxRuntime.jsxs("div", { className: "designbase-carousel__slide-meta", children: [item.meta.author && (jsxRuntime.jsx("span", { className: "designbase-carousel__slide-author", children: item.meta.author })), item.meta.date && (jsxRuntime.jsx("span", { className: "designbase-carousel__slide-date", children: item.meta.date })), item.meta.rating && (jsxRuntime.jsxs("span", { className: "designbase-carousel__slide-rating", children: ["\u2B50 ", item.meta.rating] }))] }))] })) : null] })), jsxRuntime.jsxs("div", { className: "designbase-carousel__slide-actions", children: [enableLike && (jsxRuntime.jsx("button", { className: clsx('designbase-carousel__action-button', 'designbase-carousel__action-button--like', { 'designbase-carousel__action-button--active': isLiked }), onClick: (e) => {
5407
+ }, children: item.description })), item.meta && (jsxRuntime.jsxs("div", { className: "designbase-carousel__slide-meta", children: [item.meta.author && (jsxRuntime.jsx("span", { className: "designbase-carousel__slide-author", children: item.meta.author })), item.meta.date && (jsxRuntime.jsx("span", { className: "designbase-carousel__slide-date", children: item.meta.date })), item.meta.rating && (jsxRuntime.jsxs("span", { className: "designbase-carousel__slide-rating", children: ["\u2B50 ", item.meta.rating] }))] }))] })) : null] })), jsxRuntime.jsxs("div", { className: "designbase-carousel__slide-actions", children: [enableLike && (jsxRuntime.jsx("button", { className: clsx('designbase-carousel__action-button', 'designbase-carousel__action-button--like', {
5408
+ 'designbase-carousel__action-button--active': likedItems.has(item.id),
5409
+ }), onClick: (e) => {
5263
5410
  e.stopPropagation();
5264
5411
  handleLike(item, index);
5265
- }, title: isLiked ? "좋아요 취소" : "좋아요", children: jsxRuntime.jsx(icons.HeartIcon, { size: iconSize, color: "currentColor" }) })), enableBookmark && (jsxRuntime.jsx("button", { className: clsx('designbase-carousel__action-button', 'designbase-carousel__action-button--bookmark', { 'designbase-carousel__action-button--active': isBookmarked }), onClick: (e) => {
5412
+ }, title: likedItems.has(item.id) ? "좋아요 취소" : "좋아요", children: jsxRuntime.jsx(icons.HeartIcon, { size: iconSize, color: "currentColor" }) })), enableBookmark && (jsxRuntime.jsx("button", { className: clsx('designbase-carousel__action-button', 'designbase-carousel__action-button--bookmark', {
5413
+ 'designbase-carousel__action-button--active': bookmarkedItems.has(item.id),
5414
+ }), onClick: (e) => {
5266
5415
  e.stopPropagation();
5267
5416
  handleBookmark(item, index);
5268
- }, title: isBookmarked ? "북마크 해제" : "북마크", children: jsxRuntime.jsx(icons.BookmarkIcon, { size: iconSize, color: "currentColor" }) })), enableShare && (jsxRuntime.jsx("button", { className: "designbase-carousel__action-button designbase-carousel__action-button--share", onClick: (e) => {
5417
+ }, title: bookmarkedItems.has(item.id) ? "북마크 해제" : "북마크", children: jsxRuntime.jsx(icons.BookmarkIcon, { size: iconSize, color: "currentColor" }) })), enableShare && (jsxRuntime.jsx("button", { className: "designbase-carousel__action-button designbase-carousel__action-button--share", onClick: (e) => {
5269
5418
  e.stopPropagation();
5270
5419
  handleShare(item, index);
5271
5420
  }, title: "\uACF5\uC720", children: jsxRuntime.jsx(icons.ShareAltIcon, { size: iconSize, color: "currentColor" }) })), enableDownload && item.image && (jsxRuntime.jsx("button", { className: "designbase-carousel__action-button designbase-carousel__action-button--download", onClick: (e) => {
@@ -8928,157 +9077,6 @@
8928
9077
  };
8929
9078
  Tutorial.displayName = 'Tutorial';
8930
9079
 
8931
- const SearchBar = ({ value, defaultValue = '', placeholder = '검색...', size = 'm', variant = 'default', disabled = false, readOnly = false, fullWidth = false, searchIcon: SearchIconComponent = icons.SearchIcon, clearIcon: ClearIconComponent = icons.CloseIcon, enableRecentSearches = false, recentSearchesKey = 'searchbar-recent-searches', suggestedSearches = [], suggestionRollingInterval = 5000, onChange, onSearch, onFocus, onBlur, onKeyDown, className, ...props }) => {
8932
- const [internalValue, setInternalValue] = React.useState(defaultValue);
8933
- const [recentSearches, setRecentSearches] = React.useState([]);
8934
- const [showRecentSearches, setShowRecentSearches] = React.useState(false);
8935
- const [currentSuggestion, setCurrentSuggestion] = React.useState(0);
8936
- const [isFocused, setIsFocused] = React.useState(false);
8937
- const inputRef = React.useRef(null);
8938
- const suggestionIntervalRef = React.useRef(null);
8939
- const currentValue = value !== undefined ? value : internalValue;
8940
- // 최근 검색어 로드
8941
- React.useEffect(() => {
8942
- if (enableRecentSearches) {
8943
- try {
8944
- const stored = localStorage.getItem(recentSearchesKey);
8945
- if (stored) {
8946
- setRecentSearches(JSON.parse(stored));
8947
- }
8948
- }
8949
- catch (error) {
8950
- console.warn('최근 검색어를 불러올 수 없습니다:', error);
8951
- }
8952
- }
8953
- }, [enableRecentSearches, recentSearchesKey]);
8954
- // 추천 검색어 롤링 (value가 없을 때만)
8955
- React.useEffect(() => {
8956
- if (suggestedSearches.length > 0 && !currentValue && currentValue === '') {
8957
- suggestionIntervalRef.current = setInterval(() => {
8958
- setCurrentSuggestion(prev => (prev + 1) % suggestedSearches.length);
8959
- }, suggestionRollingInterval);
8960
- }
8961
- else {
8962
- if (suggestionIntervalRef.current) {
8963
- clearInterval(suggestionIntervalRef.current);
8964
- suggestionIntervalRef.current = null;
8965
- }
8966
- }
8967
- return () => {
8968
- if (suggestionIntervalRef.current) {
8969
- clearInterval(suggestionIntervalRef.current);
8970
- }
8971
- };
8972
- }, [suggestedSearches.length, currentValue, suggestionRollingInterval]);
8973
- React.useEffect(() => {
8974
- if (value !== undefined) {
8975
- setInternalValue(value);
8976
- }
8977
- }, [value]);
8978
- const handleChange = (e) => {
8979
- const newValue = e.target.value;
8980
- setInternalValue(newValue);
8981
- onChange?.(newValue);
8982
- };
8983
- const handleClear = () => {
8984
- setInternalValue('');
8985
- onChange?.('');
8986
- inputRef.current?.focus();
8987
- };
8988
- const handleSearch = (searchValue) => {
8989
- const searchTerm = searchValue || currentValue.trim();
8990
- if (searchTerm) {
8991
- onSearch?.(searchTerm);
8992
- // 최근 검색어 저장
8993
- if (enableRecentSearches) {
8994
- const newRecentSearches = [
8995
- searchTerm,
8996
- ...recentSearches.filter(item => item !== searchTerm)
8997
- ].slice(0, 10); // 최대 10개
8998
- setRecentSearches(newRecentSearches);
8999
- try {
9000
- localStorage.setItem(recentSearchesKey, JSON.stringify(newRecentSearches));
9001
- }
9002
- catch (error) {
9003
- console.warn('최근 검색어를 저장할 수 없습니다:', error);
9004
- }
9005
- }
9006
- }
9007
- };
9008
- const handleKeyDown = (e) => {
9009
- if (e.key === 'Enter') {
9010
- handleSearch();
9011
- }
9012
- onKeyDown?.(e);
9013
- };
9014
- const handleFocus = (e) => {
9015
- setIsFocused(true);
9016
- setShowRecentSearches(enableRecentSearches && recentSearches.length > 0);
9017
- onFocus?.(e);
9018
- };
9019
- const handleBlur = (e) => {
9020
- setIsFocused(false);
9021
- // 약간의 지연을 두어 클릭 이벤트가 처리되도록 함
9022
- setTimeout(() => {
9023
- setShowRecentSearches(false);
9024
- }, 200);
9025
- onBlur?.(e);
9026
- };
9027
- const handleRecentSearchClick = (searchTerm) => {
9028
- setInternalValue(searchTerm);
9029
- onChange?.(searchTerm);
9030
- setShowRecentSearches(false);
9031
- handleSearch(searchTerm);
9032
- };
9033
- const handleRemoveRecentSearch = (searchTerm) => {
9034
- const newRecentSearches = recentSearches.filter(item => item !== searchTerm);
9035
- setRecentSearches(newRecentSearches);
9036
- try {
9037
- localStorage.setItem(recentSearchesKey, JSON.stringify(newRecentSearches));
9038
- }
9039
- catch (error) {
9040
- console.warn('최근 검색어를 삭제할 수 없습니다:', error);
9041
- }
9042
- };
9043
- const handleClearAllRecentSearches = () => {
9044
- setRecentSearches([]);
9045
- try {
9046
- localStorage.removeItem(recentSearchesKey);
9047
- }
9048
- catch (error) {
9049
- console.warn('최근 검색어를 모두 삭제할 수 없습니다:', error);
9050
- }
9051
- };
9052
- const handleSuggestionClick = (suggestion) => {
9053
- setInternalValue(suggestion);
9054
- onChange?.(suggestion);
9055
- // 롤링 중단
9056
- if (suggestionIntervalRef.current) {
9057
- clearInterval(suggestionIntervalRef.current);
9058
- suggestionIntervalRef.current = null;
9059
- }
9060
- handleSearch(suggestion);
9061
- };
9062
- const classes = clsx('designbase-search-bar', `designbase-search-bar--${size}`, `designbase-search-bar--${variant}`, {
9063
- 'designbase-search-bar--disabled': disabled,
9064
- 'designbase-search-bar--readonly': readOnly,
9065
- 'designbase-search-bar--full-width': fullWidth,
9066
- 'designbase-search-bar--has-value': currentValue && currentValue.length > 0,
9067
- }, className);
9068
- const inputClasses = clsx('designbase-search-bar__input', {
9069
- 'designbase-search-bar__input--disabled': disabled,
9070
- 'designbase-search-bar__input--readonly': readOnly,
9071
- });
9072
- // 현재 플레이스홀더 (value가 없을 때만 추천 검색어 롤링)
9073
- const currentPlaceholder = suggestedSearches.length > 0 && !currentValue && currentValue === ''
9074
- ? suggestedSearches[currentSuggestion]
9075
- : placeholder;
9076
- 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', {
9077
- 'designbase-search-bar__suggestion-item--active': index === currentSuggestion
9078
- }), onClick: () => handleSuggestionClick(suggestion), children: suggestion }, index))) })] }))] }));
9079
- };
9080
- SearchBar.displayName = 'SearchBar';
9081
-
9082
9080
  const Navbar = ({ size = 'm', variant = 'default', position = 'static', logo, onLogoClick, items = [], onItemClick, userMenuItems = [], onUserMenuItemClick, userProfile, onLoginClick, onLogoutClick, isAuthenticated = false, showSearch = false, onSearch, searchPlaceholder = '검색...', fullWidth = false, shadow = false, className, ...props }) => {
9083
9081
  const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
9084
9082
  const [isUserMenuOpen, setIsUserMenuOpen] = React.useState(false);
@@ -11591,6 +11589,47 @@
11591
11589
  });
11592
11590
  Toggle.displayName = 'Toggle';
11593
11591
 
11592
+ const clampRating = (rating, maxRating) => {
11593
+ if (typeof rating !== 'number' || Number.isNaN(rating))
11594
+ return undefined;
11595
+ if (rating < 0)
11596
+ return 0;
11597
+ if (rating > maxRating)
11598
+ return maxRating;
11599
+ return Math.round(rating * 10) / 10;
11600
+ };
11601
+ const Testimonial = ({ quote, author, role, company, avatar, rating, maxRating = 5, size = 'm', align = 'left', badge, children, className, badgeProps, avatarProps, ratingProps, }) => {
11602
+ const clampedRating = clampRating(rating, maxRating);
11603
+ const avatarSizeMap = {
11604
+ s: 's',
11605
+ m: 'm',
11606
+ l: 'l',
11607
+ };
11608
+ const ratingSizeMap = {
11609
+ s: 's',
11610
+ m: 'm',
11611
+ l: 'l',
11612
+ };
11613
+ const { className: badgeCustomClassName, size: badgeSize, variant: badgeVariant, style: badgeStyle, ...restBadgeProps } = badgeProps ?? {};
11614
+ const { className: avatarCustomClassName, size: avatarSize, alt: avatarAlt, src: avatarSrc, ...restAvatarProps } = avatarProps ?? {};
11615
+ const { className: ratingCustomClassName, maxValue: ratingMaxValueOverride, size: ratingSize, display: ratingDisplay, allowHalf: ratingAllowHalf, readonly: ratingReadonly, color: ratingColor, customColor: ratingCustomColor, ...restRatingProps } = ratingProps ?? {};
11616
+ const computedBadgeSize = badgeSize ?? 's';
11617
+ const computedBadgeVariant = badgeVariant ?? 'primary';
11618
+ const computedBadgeStyle = badgeStyle ?? 'text';
11619
+ const computedAvatarSrc = avatarSrc ?? avatar;
11620
+ const computedAvatarAlt = avatarAlt ?? `${author} 프로필 이미지`;
11621
+ const computedAvatarSize = avatarSize ?? avatarSizeMap[size];
11622
+ const computedRatingMaxValue = ratingMaxValueOverride ?? maxRating;
11623
+ const computedRatingSize = ratingSize ?? ratingSizeMap[size];
11624
+ const computedRatingDisplay = ratingDisplay ?? 'both';
11625
+ const computedRatingAllowHalf = ratingAllowHalf ?? true;
11626
+ const computedRatingReadonly = ratingReadonly ?? true;
11627
+ const computedRatingColor = ratingColor ?? 'primary';
11628
+ const computedRatingCustomColor = computedRatingColor === 'custom' ? ratingCustomColor : undefined;
11629
+ const shouldRenderAvatar = Boolean(computedAvatarSrc || restAvatarProps.initials || restAvatarProps.icon);
11630
+ return (jsxRuntime.jsxs("figure", { className: clsx('designbase-testimonial', `designbase-testimonial--size-${size}`, `designbase-testimonial--align-${align}`, className), children: [badge && (jsxRuntime.jsx(Badge, { size: computedBadgeSize, variant: computedBadgeVariant, style: computedBadgeStyle, className: clsx('designbase-testimonial__badge', badgeCustomClassName), ...restBadgeProps, children: badge })), jsxRuntime.jsxs("blockquote", { className: "designbase-testimonial__quote", children: ["\u201C", quote, "\u201D"] }), jsxRuntime.jsxs("figcaption", { className: "designbase-testimonial__footer", children: [shouldRenderAvatar && (jsxRuntime.jsx("div", { className: "designbase-testimonial__avatar", children: jsxRuntime.jsx(Avatar, { src: computedAvatarSrc, alt: computedAvatarAlt, size: computedAvatarSize, className: clsx('designbase-testimonial__avatar-image', avatarCustomClassName), ...restAvatarProps }) })), jsxRuntime.jsxs("div", { className: "designbase-testimonial__meta", children: [jsxRuntime.jsx("span", { className: "designbase-testimonial__author", children: author }), (role || company) && (jsxRuntime.jsx("span", { className: "designbase-testimonial__role", children: [role, company].filter(Boolean).join(' · ') }))] }), typeof clampedRating === 'number' && (jsxRuntime.jsx(Rating, { value: clampedRating, maxValue: computedRatingMaxValue, size: computedRatingSize, display: computedRatingDisplay, allowHalf: computedRatingAllowHalf, readonly: computedRatingReadonly, color: computedRatingColor, customColor: computedRatingCustomColor, className: clsx('designbase-testimonial__rating', ratingCustomClassName), ...restRatingProps }))] }), children && jsxRuntime.jsx("div", { className: "designbase-testimonial__extra", children: children })] }));
11631
+ };
11632
+
11594
11633
  const Toolbar = ({ items, size = 'm', variant = 'default', position = 'top', fullWidth = false, fixed = false, shadow = true, rounded = true, className, children, }) => {
11595
11634
  const [openDropdown, setOpenDropdown] = React.useState(null);
11596
11635
  const mapSizeToButtonSize = (size) => {
@@ -12220,6 +12259,7 @@
12220
12259
  exports.Stepper = Stepper;
12221
12260
  exports.Table = Table;
12222
12261
  exports.Tabs = Tabs;
12262
+ exports.Testimonial = Testimonial;
12223
12263
  exports.Textarea = Textarea;
12224
12264
  exports.TimePicker = TimePicker;
12225
12265
  exports.Timeline = Timeline;