@designbasekorea/ui 0.2.22 → 0.2.25

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, createContext } 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, PaletteIcon, 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, CaretUpdownFilledIcon, DeleteIcon, WarningIcon, InfoIcon } 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, EyedropperIcon, 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, CaretUpdownFilledIcon, DeleteIcon, WarningIcon, InfoIcon } 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}
@@ -5277,247 +5277,366 @@ const Chip = ({ label, size = 'm', variant = 'default', color = 'primary', delet
5277
5277
  };
5278
5278
  Chip.displayName = 'Chip';
5279
5279
 
5280
- const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left', value, defaultValue = '#006FFF', showInput = true, showAlpha = false, showFormatSelector = true, showCopyButton = true, disabled = false, readonly = false, onChange, className, }) => {
5281
- const [selectedColor, setSelectedColor] = useState(value || defaultValue);
5280
+ /** 유틸 */
5281
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
5282
+ const hexToRgb = (hex) => {
5283
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
5284
+ if (!m)
5285
+ return null;
5286
+ return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
5287
+ };
5288
+ const rgbToHex = (r, g, b) => {
5289
+ const toHex = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');
5290
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
5291
+ };
5292
+ const rgbToHsv = (r, g, b) => {
5293
+ r /= 255;
5294
+ g /= 255;
5295
+ b /= 255;
5296
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
5297
+ const d = max - min;
5298
+ let h = 0;
5299
+ if (d !== 0) {
5300
+ switch (max) {
5301
+ case r:
5302
+ h = ((g - b) / d + (g < b ? 6 : 0));
5303
+ break;
5304
+ case g:
5305
+ h = ((b - r) / d + 2);
5306
+ break;
5307
+ case b:
5308
+ h = ((r - g) / d + 4);
5309
+ break;
5310
+ }
5311
+ h *= 60;
5312
+ }
5313
+ const s = max === 0 ? 0 : d / max;
5314
+ const v = max;
5315
+ return { h: Math.round(h * 100) / 100, s: Math.round(s * 10000) / 100, v: Math.round(v * 10000) / 100 };
5316
+ };
5317
+ const hsvToRgb = (h, s, v) => {
5318
+ s /= 100;
5319
+ v /= 100;
5320
+ const c = v * s;
5321
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
5322
+ const m = v - c;
5323
+ let rp = 0, gp = 0, bp = 0;
5324
+ if (h >= 0 && h < 60) {
5325
+ rp = c;
5326
+ gp = x;
5327
+ bp = 0;
5328
+ }
5329
+ else if (h < 120) {
5330
+ rp = x;
5331
+ gp = c;
5332
+ bp = 0;
5333
+ }
5334
+ else if (h < 180) {
5335
+ rp = 0;
5336
+ gp = c;
5337
+ bp = x;
5338
+ }
5339
+ else if (h < 240) {
5340
+ rp = 0;
5341
+ gp = x;
5342
+ bp = c;
5343
+ }
5344
+ else if (h < 300) {
5345
+ rp = x;
5346
+ gp = 0;
5347
+ bp = c;
5348
+ }
5349
+ else {
5350
+ rp = c;
5351
+ gp = 0;
5352
+ bp = x;
5353
+ }
5354
+ return { r: Math.round((rp + m) * 255), g: Math.round((gp + m) * 255), b: Math.round((bp + m) * 255) };
5355
+ };
5356
+ const rgbToHsl = (r, g, b) => {
5357
+ r /= 255;
5358
+ g /= 255;
5359
+ b /= 255;
5360
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
5361
+ let h = 0, s = 0;
5362
+ const l = (max + min) / 2;
5363
+ if (max !== min) {
5364
+ const d = max - min;
5365
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
5366
+ switch (max) {
5367
+ case r:
5368
+ h = ((g - b) / d + (g < b ? 6 : 0));
5369
+ break;
5370
+ case g:
5371
+ h = ((b - r) / d + 2);
5372
+ break;
5373
+ case b:
5374
+ h = ((r - g) / d + 4);
5375
+ break;
5376
+ }
5377
+ h *= 60;
5378
+ }
5379
+ return { h: Math.round(h * 100) / 100, s: Math.round(s * 10000) / 100, l: Math.round(l * 10000) / 100 };
5380
+ };
5381
+ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left', value, defaultValue = '#006FFF', showInput = true, showAlpha = true, showFormatSelector = true, showCopyButton = true, disabled = false, readonly = false, onChange, onApply, onCancel, className, }) => {
5382
+ /** 내부 상태 (HSV + alpha%) */
5383
+ const [h, setH] = useState(211);
5384
+ const [s, setS] = useState(100);
5385
+ const [v, setV] = useState(50);
5386
+ const [a, setA] = useState(100);
5282
5387
  const [isOpen, setIsOpen] = useState(false);
5283
- const [hue, setHue] = useState(211);
5284
- const [saturation, setSaturation] = useState(100);
5285
- const [lightness, setLightness] = useState(50);
5286
- const [alpha, setAlpha] = useState(100);
5287
- const [colorFormat, setColorFormat] = useState('hex');
5388
+ const [format, setFormat] = useState('hex');
5288
5389
  const [isCopied, setIsCopied] = useState(false);
5289
- const [inputValue, setInputValue] = useState(value || defaultValue);
5390
+ // 하단 컬러 인풋(문자열), 알파 인풋(문자열) 엔터로만 확정
5391
+ const [colorInput, setColorInput] = useState('');
5392
+ const [alphaInput, setAlphaInput] = useState('100');
5393
+ // 모달용 임시 상태 (적용/취소 버튼용)
5394
+ const [tempColor, setTempColor] = useState('');
5290
5395
  const pickerRef = useRef(null);
5291
- // 초기화: defaultValue가 있으면 HSL 값으로 변환
5396
+ const areaRef = useRef(null);
5397
+ const dragging = useRef(false);
5398
+ /** 초기화 */
5292
5399
  useEffect(() => {
5293
- const initialColor = value || defaultValue;
5294
- if (initialColor) {
5295
- updateHSLFromHex(initialColor);
5296
- setInputValue(initialColor);
5400
+ const initial = (value || defaultValue).toUpperCase();
5401
+ const rgb = hexToRgb(initial);
5402
+ if (rgb) {
5403
+ const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);
5404
+ setH(h);
5405
+ setS(s);
5406
+ setV(v);
5407
+ setColorInput(initial);
5408
+ setAlphaInput('100');
5297
5409
  }
5298
- }, []); // 컴포넌트 마운트 시에만 실행
5299
- // value prop이 변경되면 업데이트
5410
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5411
+ }, []);
5412
+ /** 외부 value 변화 */
5300
5413
  useEffect(() => {
5301
- if (value) {
5302
- setSelectedColor(value);
5303
- updateHSLFromHex(value);
5304
- }
5305
- }, [value]);
5306
- // HEX를 HSL로 변환
5307
- const updateHSLFromHex = (hex) => {
5308
- const rgb = hexToRgb(hex);
5414
+ if (!value)
5415
+ return;
5416
+ const rgb = hexToRgb(value);
5309
5417
  if (rgb) {
5310
- const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
5311
- setHue(hsl.h);
5312
- setSaturation(hsl.s);
5313
- setLightness(hsl.l);
5314
- }
5315
- };
5316
- // HEX를 RGB로 변환
5317
- const hexToRgb = (hex) => {
5318
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
5319
- return result ? {
5320
- r: parseInt(result[1], 16),
5321
- g: parseInt(result[2], 16),
5322
- b: parseInt(result[3], 16)
5323
- } : null;
5324
- };
5325
- // RGB를 HSL로 변환 (정밀도 개선)
5326
- const rgbToHsl = (r, g, b) => {
5327
- r /= 255;
5328
- g /= 255;
5329
- b /= 255;
5330
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
5331
- let h = 0, s = 0, l = (max + min) / 2;
5332
- if (max !== min) {
5333
- const d = max - min;
5334
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
5335
- switch (max) {
5336
- case r:
5337
- h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
5338
- break;
5339
- case g:
5340
- h = ((b - r) / d + 2) / 6;
5341
- break;
5342
- case b:
5343
- h = ((r - g) / d + 4) / 6;
5344
- break;
5345
- }
5418
+ const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);
5419
+ setH(h);
5420
+ setS(s);
5421
+ setV(v);
5346
5422
  }
5347
- // 정밀도 개선: 반올림 대신 더 정확한 계산
5348
- return {
5349
- h: Math.round(h * 360 * 100) / 100,
5350
- s: Math.round(s * 100 * 100) / 100,
5351
- l: Math.round(l * 100 * 100) / 100
5352
- };
5353
- };
5354
- // HSL을 RGB로 변환 (정밀도 개선)
5355
- const hslToRgb = (h, s, l) => {
5356
- l /= 100;
5357
- const a = s * Math.min(l, 1 - l) / 100;
5358
- const f = (n) => {
5359
- const k = (n + h / 30) % 12;
5360
- const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
5361
- return Math.round(255 * color);
5362
- };
5363
- return { r: f(0), g: f(8), b: f(4) };
5364
- };
5365
- // HSL을 HEX로 변환 (정밀도 개선)
5366
- const hslToHex = (h, s, l) => {
5367
- const rgb = hslToRgb(h, s, l);
5368
- const toHex = (n) => {
5369
- // RGB 값을 0-255 범위로 제한하고 HEX로 변환
5370
- const clamped = Math.max(0, Math.min(255, Math.round(n)));
5371
- return clamped.toString(16).padStart(2, '0');
5423
+ }, [value]);
5424
+ /** 파생값 */
5425
+ const rgb = useMemo(() => hsvToRgb(h, s, v), [h, s, v]);
5426
+ const hex = useMemo(() => rgbToHex(rgb.r, rgb.g, rgb.b), [rgb]);
5427
+ const hsl = useMemo(() => rgbToHsl(rgb.r, rgb.g, rgb.b), [rgb]);
5428
+ /** 표시 문자열 */
5429
+ const formatted = useMemo(() => {
5430
+ const alpha = a / 100;
5431
+ switch (format) {
5432
+ case 'hex': return hex;
5433
+ case 'rgb': return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
5434
+ case 'rgba': return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha.toFixed(2)})`;
5435
+ case 'hsl': return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
5436
+ case 'hsla': return `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${alpha.toFixed(2)})`;
5437
+ default: return hex;
5438
+ }
5439
+ }, [format, hex, rgb, hsl, a]);
5440
+ /** 외부 onChange 및 인풋 동기화 */
5441
+ useEffect(() => {
5442
+ onChange?.(formatted);
5443
+ setColorInput(formatted.toUpperCase());
5444
+ setAlphaInput(String(a));
5445
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5446
+ }, [formatted]);
5447
+ /** 드롭다운 외부 클릭 닫기 */
5448
+ useEffect(() => {
5449
+ const handler = (e) => {
5450
+ if (pickerRef.current && !pickerRef.current.contains(e.target))
5451
+ setIsOpen(false);
5372
5452
  };
5373
- return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`.toUpperCase();
5374
- };
5375
- // 현재 포맷에 따른 색상 문자열 반환
5376
- const getFormattedColor = () => {
5377
- const rgb = hslToRgb(hue, saturation, lightness);
5378
- const alphaValue = alpha / 100;
5379
- switch (colorFormat) {
5380
- case 'hex':
5381
- return hslToHex(hue, saturation, lightness);
5382
- case 'rgb':
5383
- return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
5384
- case 'rgba':
5385
- return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alphaValue.toFixed(2)})`;
5386
- case 'hsl':
5387
- return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
5388
- case 'hsla':
5389
- return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alphaValue.toFixed(2)})`;
5390
- default:
5391
- return hslToHex(hue, saturation, lightness);
5392
- }
5393
- };
5394
- // 색상 변경 핸들러
5395
- const handleColorChange = (newColor) => {
5396
- setSelectedColor(newColor);
5397
- onChange?.(newColor);
5398
- };
5399
- // Hue 슬라이더 변경
5400
- const handleHueChange = (e) => {
5401
- const newHue = parseInt(e.target.value);
5402
- setHue(newHue);
5403
- const newColor = hslToHex(newHue, saturation, lightness);
5404
- handleColorChange(newColor);
5405
- };
5406
- // Alpha 슬라이더 변경
5407
- const handleAlphaChange = (e) => {
5408
- const newAlpha = parseInt(e.target.value);
5409
- setAlpha(newAlpha);
5410
- const newColor = getFormattedColorWithAlpha(newAlpha);
5411
- handleColorChange(newColor);
5412
- };
5413
- // Alpha 값을 포함한 색상 문자열 반환
5414
- const getFormattedColorWithAlpha = (alphaVal) => {
5415
- const rgb = hslToRgb(hue, saturation, lightness);
5416
- const alphaValue = alphaVal / 100;
5417
- if (colorFormat === 'rgba') {
5418
- return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alphaValue.toFixed(2)})`;
5419
- }
5420
- else if (colorFormat === 'hsla') {
5421
- return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alphaValue.toFixed(2)})`;
5453
+ if (isOpen && type === 'dropdown')
5454
+ document.addEventListener('mousedown', handler);
5455
+ return () => document.removeEventListener('mousedown', handler);
5456
+ }, [isOpen, type]);
5457
+ /** 컬러 필드 핸들링 */
5458
+ const updateFromArea = (clientX, clientY) => {
5459
+ const el = areaRef.current;
5460
+ if (!el)
5461
+ return;
5462
+ const rect = el.getBoundingClientRect();
5463
+ const x = clamp(clientX - rect.left, 0, rect.width);
5464
+ const y = clamp(clientY - rect.top, 0, rect.height);
5465
+ const newS = Math.round((x / rect.width) * 100);
5466
+ const newV = Math.round(100 - (y / rect.height) * 100);
5467
+ setS(newS);
5468
+ setV(newV);
5469
+ };
5470
+ const onAreaMouseDown = (e) => { dragging.current = true; updateFromArea(e.clientX, e.clientY); };
5471
+ const onAreaMouseMove = (e) => { if (dragging.current)
5472
+ updateFromArea(e.clientX, e.clientY); };
5473
+ const onAreaMouseUp = () => { dragging.current = false; };
5474
+ const onAreaLeave = () => { dragging.current = false; };
5475
+ const onAreaTouchStart = (e) => { dragging.current = true; const t = e.touches[0]; updateFromArea(t.clientX, t.clientY); };
5476
+ const onAreaTouchMove = (e) => { if (!dragging.current)
5477
+ return; const t = e.touches[0]; updateFromArea(t.clientX, t.clientY); };
5478
+ const onAreaTouchEnd = () => { dragging.current = false; };
5479
+ /** 슬라이더 */
5480
+ const onHueChange = (e) => setH(parseInt(e.target.value, 10));
5481
+ const onAlphaChange = (e) => {
5482
+ const newAlpha = parseInt(e.target.value, 10);
5483
+ setA(newAlpha);
5484
+ setAlphaInput(String(newAlpha)); // 실시간 동기화
5485
+ };
5486
+ /** 컬러 인풋: 엔터로만 확정 */
5487
+ const tryCommitColorInput = () => {
5488
+ const str = colorInput.trim();
5489
+ // HEX
5490
+ if (/^#([0-9A-Fa-f]{6})$/.test(str)) {
5491
+ const rgb = hexToRgb(str);
5492
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
5493
+ setH(hsv.h);
5494
+ setS(hsv.s);
5495
+ setV(hsv.v);
5496
+ return;
5497
+ }
5498
+ // rgb()
5499
+ let m = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(str);
5500
+ if (m) {
5501
+ const r = clamp(parseInt(m[1], 10), 0, 255);
5502
+ const g = clamp(parseInt(m[2], 10), 0, 255);
5503
+ const b = clamp(parseInt(m[3], 10), 0, 255);
5504
+ const hsv = rgbToHsv(r, g, b);
5505
+ setH(hsv.h);
5506
+ setS(hsv.s);
5507
+ setV(hsv.v);
5508
+ return;
5509
+ }
5510
+ // rgba()
5511
+ m = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0?\.\d+)\s*\)$/i.exec(str);
5512
+ if (m) {
5513
+ const r = clamp(parseInt(m[1], 10), 0, 255);
5514
+ const g = clamp(parseInt(m[2], 10), 0, 255);
5515
+ const b = clamp(parseInt(m[3], 10), 0, 255);
5516
+ const alpha = parseFloat(m[4]);
5517
+ const hsv = rgbToHsv(r, g, b);
5518
+ setH(hsv.h);
5519
+ setS(hsv.s);
5520
+ setV(hsv.v);
5521
+ setA(Math.round(alpha * 100));
5522
+ return;
5523
+ }
5524
+ // 실패 → 원래 값으로 복구
5525
+ setColorInput(formatted.toUpperCase());
5526
+ };
5527
+ const onColorKeyDown = (e) => {
5528
+ if (e.key === 'Enter')
5529
+ tryCommitColorInput();
5530
+ };
5531
+ const onColorBlur = () => {
5532
+ // 요구사항: 엔터로만 변경. 블러 시에는 값 복구만.
5533
+ setColorInput(formatted.toUpperCase());
5534
+ };
5535
+ /** 알파 인풋: 엔터로만 확정 (0-100) */
5536
+ const tryCommitAlphaInput = () => {
5537
+ const n = Number(alphaInput.trim());
5538
+ if (!Number.isNaN(n) && n >= 0 && n <= 100) {
5539
+ setA(Math.round(n));
5540
+ return;
5541
+ }
5542
+ // 실패 → 복구
5543
+ setAlphaInput(String(a));
5544
+ };
5545
+ const onAlphaInputKeyDown = (e) => {
5546
+ if (e.key === 'Enter')
5547
+ tryCommitAlphaInput();
5548
+ };
5549
+ const onAlphaInputBlur = () => {
5550
+ // 엔터로만 반영. 블러 시 복원.
5551
+ setAlphaInput(String(a));
5552
+ };
5553
+ /** 복사 */
5554
+ const onCopy = async () => {
5555
+ try {
5556
+ await navigator.clipboard.writeText(formatted);
5557
+ setIsCopied(true);
5558
+ setTimeout(() => setIsCopied(false), 1600);
5422
5559
  }
5423
- return hslToHex(hue, saturation, lightness);
5424
- };
5425
- // 입력 필드 변경
5426
- const handleInputChange = (e) => {
5427
- const newValue = e.target.value.trim();
5428
- setInputValue(newValue);
5429
- // HEX 값이 유효하면 실시간으로 색상 업데이트
5430
- if (/^#[0-9A-F]{6}$/i.test(newValue)) {
5431
- const normalizedHex = newValue.toUpperCase();
5432
- handleColorChange(normalizedHex);
5433
- updateHSLFromHex(normalizedHex);
5560
+ catch (e) {
5561
+ console.error(e);
5434
5562
  }
5435
5563
  };
5436
- // 입력 필드에서 포커스 아웃 시 검증 및 적용
5437
- const handleInputBlur = () => {
5438
- let isValid = false;
5439
- // HEX 검증
5440
- if (/^#[0-9A-F]{6}$/i.test(inputValue)) {
5441
- handleColorChange(inputValue.toUpperCase());
5442
- updateHSLFromHex(inputValue);
5443
- isValid = true;
5444
- }
5445
- // RGB 검증
5446
- else if (/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i.test(inputValue)) {
5447
- const match = inputValue.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
5448
- if (match) {
5449
- const r = parseInt(match[1]);
5450
- const g = parseInt(match[2]);
5451
- const b = parseInt(match[3]);
5452
- if (r <= 255 && g <= 255 && b <= 255) {
5453
- const hsl = rgbToHsl(r, g, b);
5454
- setHue(hsl.h);
5455
- setSaturation(hsl.s);
5456
- setLightness(hsl.l);
5457
- const hex = hslToHex(hsl.h, hsl.s, hsl.l);
5458
- handleColorChange(hex);
5459
- isValid = true;
5460
- }
5564
+ /** EyeDropper */
5565
+ const onEyedrop = async () => {
5566
+ try {
5567
+ if ('EyeDropper' in window) {
5568
+ const eye = new window.EyeDropper();
5569
+ const { sRGBHex } = await eye.open();
5570
+ const rgb = hexToRgb(sRGBHex);
5571
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
5572
+ setH(hsv.h);
5573
+ setS(hsv.s);
5574
+ setV(hsv.v);
5461
5575
  }
5462
- }
5463
- // RGBA 검증
5464
- else if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/i.test(inputValue)) {
5465
- const match = inputValue.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/i);
5466
- if (match) {
5467
- const r = parseInt(match[1]);
5468
- const g = parseInt(match[2]);
5469
- const b = parseInt(match[3]);
5470
- const a = parseFloat(match[4]);
5471
- if (r <= 255 && g <= 255 && b <= 255 && a >= 0 && a <= 1) {
5472
- const hsl = rgbToHsl(r, g, b);
5473
- setHue(hsl.h);
5474
- setSaturation(hsl.s);
5475
- setLightness(hsl.l);
5476
- setAlpha(Math.round(a * 100));
5477
- const hex = hslToHex(hsl.h, hsl.s, hsl.l);
5478
- handleColorChange(hex);
5479
- isValid = true;
5480
- }
5576
+ else {
5577
+ console.log('EyeDropper API is not supported');
5481
5578
  }
5482
5579
  }
5483
- // 유효하지 않으면 이전 값으로 복원
5484
- if (!isValid) {
5485
- setInputValue(getFormattedColor());
5580
+ catch (e) {
5581
+ console.error(e);
5486
5582
  }
5487
5583
  };
5488
- // 복사 기능
5489
- const handleCopy = async () => {
5490
- try {
5491
- await navigator.clipboard.writeText(getFormattedColor());
5492
- setIsCopied(true);
5493
- setTimeout(() => setIsCopied(false), 2000);
5584
+ /** 모달용 핸들러 */
5585
+ const handleModalOpen = () => {
5586
+ if (type === 'modal') {
5587
+ setTempColor(formatted);
5494
5588
  }
5495
- catch (err) {
5496
- console.error('Failed to copy:', err);
5589
+ setIsOpen(true);
5590
+ };
5591
+ const handleModalApply = () => {
5592
+ if (type === 'modal') {
5593
+ onApply?.(formatted);
5594
+ setIsOpen(false);
5497
5595
  }
5498
5596
  };
5499
- // 포맷 변경 inputValue 업데이트
5500
- useEffect(() => {
5501
- setInputValue(getFormattedColor());
5502
- }, [colorFormat, hue, saturation, lightness, alpha]);
5503
- // 외부 클릭 감지
5504
- useEffect(() => {
5505
- const handleClickOutside = (event) => {
5506
- if (pickerRef.current && !pickerRef.current.contains(event.target)) {
5507
- setIsOpen(false);
5508
- }
5509
- };
5510
- if (isOpen && type === 'dropdown') {
5511
- document.addEventListener('mousedown', handleClickOutside);
5597
+ const handleModalCancel = () => {
5598
+ if (type === 'modal') {
5599
+ // 원래 색상으로 복원
5600
+ const rgb = hexToRgb(tempColor);
5601
+ if (rgb) {
5602
+ const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);
5603
+ setH(h);
5604
+ setS(s);
5605
+ setV(v);
5606
+ }
5607
+ onCancel?.();
5608
+ setIsOpen(false);
5512
5609
  }
5513
- return () => {
5514
- document.removeEventListener('mousedown', handleClickOutside);
5515
- };
5516
- }, [isOpen, type]);
5517
- const togglePicker = () => {
5518
- if (disabled || readonly)
5519
- return;
5520
- setIsOpen(!isOpen);
5610
+ };
5611
+ /** 배경 스타일 */
5612
+ const hueTrackStyle = {
5613
+ background: `linear-gradient(to right,
5614
+ hsl(0,100%,50%),
5615
+ hsl(60,100%,50%),
5616
+ hsl(120,100%,50%),
5617
+ hsl(180,100%,50%),
5618
+ hsl(240,100%,50%),
5619
+ hsl(300,100%,50%),
5620
+ hsl(360,100%,50%))`,
5621
+ };
5622
+ const areaBackground = {
5623
+ backgroundImage: `
5624
+ linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)),
5625
+ linear-gradient(to right, #ffffff, hsl(${h}, 100%, 50%))
5626
+ `,
5627
+ };
5628
+ /** ✔︎ 알파 트랙: 색상 무관, 고정 흑백 그라디언트 */
5629
+ const alphaTrackStyle = {
5630
+ backgroundImage: `
5631
+ linear-gradient(45deg, var(--db-border-base) 25%, transparent 25%),
5632
+ linear-gradient(-45deg, var(--db-border-base) 25%, transparent 25%),
5633
+ linear-gradient(45deg, transparent 75%, var(--db-border-base) 75%),
5634
+ linear-gradient(-45deg, transparent 75%, var(--db-border-base) 75%),
5635
+ linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))
5636
+ `,
5637
+ backgroundSize: '8px 8px,8px 8px,8px 8px,8px 8px,100% 100%',
5638
+ backgroundPosition: '0 0,0 4px,4px -4px,-4px 0,0 0',
5639
+ backgroundColor: 'var(--db-surface-base)',
5521
5640
  };
5522
5641
  const classes = clsx('designbase-color-picker', `designbase-color-picker--${size}`, `designbase-color-picker--${position}`, {
5523
5642
  'designbase-color-picker--disabled': disabled,
@@ -5525,62 +5644,15 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5525
5644
  'designbase-color-picker--open': isOpen,
5526
5645
  'designbase-color-picker--no-input': !showInput,
5527
5646
  }, className);
5528
- // 메인 컬러 선택 영역 클릭 핸들러
5529
- const handleColorAreaClick = (e) => {
5530
- const rect = e.currentTarget.getBoundingClientRect();
5531
- const x = e.clientX - rect.left;
5532
- const y = e.clientY - rect.top;
5533
- // 좌측은 채도 낮고, 우측은 채도 최대치
5534
- const saturation = Math.round((x / rect.width) * 100);
5535
- // 위로는 밝기 최대, 아래로는 밝기 어둡게
5536
- const lightness = Math.round(100 - (y / rect.height) * 100);
5537
- setSaturation(Math.max(0, Math.min(100, saturation)));
5538
- setLightness(Math.max(0, Math.min(100, lightness)));
5539
- const newColor = hslToHex(hue, saturation, lightness);
5540
- handleColorChange(newColor);
5541
- };
5542
- // 스포이드 기능 (색상 추출)
5543
- const handleEyedropperClick = async () => {
5544
- try {
5545
- // EyeDropper API 지원 확인
5546
- if ('EyeDropper' in window) {
5547
- const eyeDropper = new window.EyeDropper();
5548
- const result = await eyeDropper.open();
5549
- const hexColor = result.sRGBHex;
5550
- handleColorChange(hexColor);
5551
- updateHSLFromHex(hexColor);
5552
- }
5553
- else {
5554
- // EyeDropper API가 지원되지 않는 경우 대체 기능
5555
- console.log('EyeDropper API is not supported in this browser');
5556
- // 여기에 대체 기능을 구현할 수 있습니다
5557
- }
5558
- }
5559
- catch (err) {
5560
- console.error('EyeDropper failed:', err);
5561
- }
5562
- };
5563
- // 컬러 선택 UI
5564
- const renderColorSelector = () => (jsxs("div", { className: "designbase-color-picker__selector", children: [jsx("div", { className: "designbase-color-picker__color-area", children: jsx("div", { className: "designbase-color-picker__color-field", style: {
5565
- background: `linear-gradient(to right,
5566
- hsl(${hue}, 0%, 50%),
5567
- hsl(${hue}, 100%, 50%)),
5568
- linear-gradient(to bottom,
5569
- hsl(${hue}, 100%, 100%),
5570
- hsl(${hue}, 100%, 0%))`
5571
- }, onClick: handleColorAreaClick, children: jsx("div", { className: "designbase-color-picker__color-pointer", style: {
5572
- left: `${saturation}%`,
5573
- top: `${100 - lightness}%`,
5574
- backgroundColor: `hsl(${hue}, ${saturation}%, ${lightness}%)`
5575
- } }) }) }), jsxs("div", { className: "designbase-color-picker__controls", children: [jsx("button", { type: "button", className: "designbase-color-picker__eyedropper", onClick: handleEyedropperClick, "aria-label": "Eyedropper tool", children: jsx(PaletteIcon, { size: 16 }) }), jsx("div", { className: "designbase-color-picker__hue-slider", children: jsx("input", { type: "range", min: "0", max: "360", value: hue, onChange: handleHueChange, className: "designbase-color-picker__slider designbase-color-picker__slider--hue" }) }), showAlpha && (jsx("div", { className: "designbase-color-picker__alpha-slider", children: jsx("input", { type: "range", min: "0", max: "100", value: alpha, onChange: handleAlphaChange, className: "designbase-color-picker__slider designbase-color-picker__slider--alpha" }) }))] }), jsxs("div", { className: "designbase-color-picker__value-display", children: [showFormatSelector && (jsxs("select", { className: "designbase-color-picker__format-select", value: colorFormat, onChange: (e) => setColorFormat(e.target.value), children: [jsx("option", { value: "hex", children: "HEX" }), jsx("option", { value: "rgb", children: "RGB" }), jsx("option", { value: "rgba", children: "RGBA" }), jsx("option", { value: "hsl", children: "HSL" }), jsx("option", { value: "hsla", children: "HSLA" })] })), jsx("input", { type: "text", value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, className: "designbase-color-picker__value-input", placeholder: "#000000" }), showAlpha && (jsxs("span", { className: "designbase-color-picker__alpha-percent", children: [alpha, "%"] })), showCopyButton && (jsx("button", { type: "button", className: "designbase-color-picker__copy-button", onClick: handleCopy, "aria-label": "Copy color value", children: isCopied ? jsx(DoneIcon$1, { size: 14 }) : jsx(CopyIcon, { size: 14 }) }))] })] }));
5576
- return (jsxs("div", { ref: pickerRef, className: classes, children: [jsxs("div", { className: "designbase-color-picker__trigger", children: [jsx("div", { className: "designbase-color-picker__color-display", onClick: togglePicker, children: jsx("div", { className: "designbase-color-picker__color-box", style: {
5577
- backgroundColor: showAlpha
5578
- ? `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha / 100})`
5579
- : `hsl(${hue}, ${saturation}%, ${lightness}%)`
5580
- } }) }), showInput && (jsx("input", { type: "text", value: inputValue, onChange: handleInputChange, onBlur: handleInputBlur, onClick: (e) => e.stopPropagation(), disabled: disabled, readOnly: readonly, className: "designbase-color-picker__input", placeholder: "#000000" })), showInput && showCopyButton && (jsx("button", { type: "button", className: "designbase-color-picker__copy-button-inline", onClick: (e) => {
5581
- e.stopPropagation();
5582
- handleCopy();
5583
- }, disabled: disabled, "aria-label": "Copy color value", children: isCopied ? jsx(DoneIcon$1, { size: 14 }) : jsx(CopyIcon, { size: 14 }) })), jsx("button", { type: "button", className: "designbase-color-picker__toggle", onClick: togglePicker, disabled: disabled, "aria-label": "Toggle color picker", children: jsx(ChevronDownIcon, { size: 16 }) })] }), type === 'dropdown' && isOpen && (jsx("div", { className: "designbase-color-picker__dropdown", children: renderColorSelector() })), type === 'modal' && (jsx(Modal, { isOpen: isOpen, onClose: () => setIsOpen(false), title: "\uC0C9\uC0C1 \uC120\uD0DD", size: "s", children: renderColorSelector() }))] }));
5647
+ const Trigger = (jsxs("div", { className: "designbase-color-picker__trigger", onClick: () => !disabled && !readonly && handleModalOpen(), children: [jsx("div", { className: "designbase-color-picker__color-display", children: jsx("div", { className: "designbase-color-picker__color-box", style: { backgroundColor: showAlpha ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a / 100})` : hex } }) }), showInput && (jsx("input", { type: "text", value: colorInput, onChange: (e) => setColorInput(e.target.value), onKeyDown: onColorKeyDown, onBlur: onColorBlur, onClick: (e) => e.stopPropagation(), disabled: disabled, readOnly: readonly, className: "designbase-color-picker__input", placeholder: "#000000" })), showInput && showCopyButton && (jsx("button", { type: "button", className: "designbase-color-picker__copy-button-inline", onClick: (e) => { e.stopPropagation(); onCopy(); }, disabled: disabled, "aria-label": "Copy color value", children: isCopied ? jsx(DoneIcon$1, { size: 14 }) : jsx(CopyIcon, { size: 14 }) })), jsx("button", { type: "button", className: "designbase-color-picker__toggle", disabled: disabled, "aria-label": "Toggle color picker", children: jsx(ChevronDownIcon, { size: 16 }) })] }));
5648
+ const Selector = (jsxs("div", { className: "designbase-color-picker__selector", children: [jsx("div", { className: "designbase-color-picker__color-area", children: jsx("div", { ref: areaRef, className: "designbase-color-picker__color-field", style: areaBackground, onMouseDown: onAreaMouseDown, onMouseMove: onAreaMouseMove, onMouseUp: onAreaMouseUp, onMouseLeave: onAreaLeave, onTouchStart: onAreaTouchStart, onTouchMove: onAreaTouchMove, onTouchEnd: onAreaTouchEnd, children: jsx("div", { className: "designbase-color-picker__color-pointer", style: { left: `${s}%`, top: `${100 - v}%`, backgroundColor: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` } }) }) }), jsxs("div", { className: "designbase-color-picker__controls", children: [jsx("button", { type: "button", className: "designbase-color-picker__eyedropper", onClick: onEyedrop, "aria-label": "Eyedropper tool", children: jsx(EyedropperIcon, { size: 16 }) }), jsx("div", { className: "designbase-color-picker__hue-slider", children: jsx("input", { type: "range", min: 0, max: 360, value: h, onChange: onHueChange, className: "designbase-color-picker__slider designbase-color-picker__slider--hue", style: hueTrackStyle }) }), showAlpha && (jsx("div", { className: "designbase-color-picker__alpha-slider", children: jsx("input", { type: "range", min: 0, max: 100, value: a, onChange: onAlphaChange, className: "designbase-color-picker__slider designbase-color-picker__slider--alpha", style: alphaTrackStyle }) }))] }), jsxs("div", { className: "designbase-color-picker__value-display", children: [showFormatSelector && (jsx(Select, { value: format, onChange: (v) => setFormat(v), showClearButton: false, options: [
5649
+ { label: 'HEX', value: 'hex' },
5650
+ { label: 'RGB', value: 'rgb' },
5651
+ { label: 'RGBA', value: 'rgba' },
5652
+ { label: 'HSL', value: 'hsl' },
5653
+ { label: 'HSLA', value: 'hsla' },
5654
+ ], size: "s" })), jsx("input", { type: "text", value: colorInput, onChange: (e) => setColorInput(e.target.value), onKeyDown: onColorKeyDown, onBlur: onColorBlur, className: "designbase-color-picker__value-input", placeholder: "#000000" }), showAlpha && (jsxs("div", { className: "designbase-color-picker__alpha-input-wrap", children: [jsx("input", { type: "text", inputMode: "numeric", value: alphaInput, onChange: (e) => setAlphaInput(e.target.value.replace(/[^\d]/g, '').slice(0, 3)), onKeyDown: onAlphaInputKeyDown, onBlur: onAlphaInputBlur, className: "designbase-color-picker__alpha-input", "aria-label": "Alpha percent" }), jsx("span", { className: "designbase-color-picker__alpha-suffix", children: "%" })] })), showCopyButton && (jsx("button", { type: "button", className: "designbase-color-picker__copy-button", onClick: onCopy, "aria-label": "Copy color value", children: isCopied ? jsx(DoneIcon$1, { size: 14 }) : jsx(CopyIcon, { size: 14 }) }))] })] }));
5655
+ return (jsxs("div", { ref: pickerRef, className: classes, children: [Trigger, type === 'dropdown' && isOpen && (jsx("div", { className: "designbase-color-picker__dropdown", onClick: (e) => e.stopPropagation(), children: Selector })), type === 'modal' && (jsxs(Modal, { isOpen: isOpen, onClose: handleModalCancel, title: "\uC0C9\uC0C1 \uC120\uD0DD", size: "s", children: [Selector, jsx(ModalFooter, { children: jsxs("div", { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' }, children: [jsx(Button, { variant: "secondary", size: "s", onClick: handleModalCancel, children: "\uCDE8\uC18C" }), jsx(Button, { variant: "primary", size: "s", onClick: handleModalApply, children: "\uC801\uC6A9" })] }) })] }))] }));
5584
5656
  };
5585
5657
  ColorPicker.displayName = 'ColorPicker';
5586
5658