@designbasekorea/ui 0.2.29 → 0.2.31

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
@@ -5352,7 +5352,11 @@ const hsvToRgb = (h, s, v) => {
5352
5352
  gp = 0;
5353
5353
  bp = x;
5354
5354
  }
5355
- return { r: Math.round((rp + m) * 255), g: Math.round((gp + m) * 255), b: Math.round((bp + m) * 255) };
5355
+ return {
5356
+ r: Math.round((rp + m) * 255),
5357
+ g: Math.round((gp + m) * 255),
5358
+ b: Math.round((bp + m) * 255),
5359
+ };
5356
5360
  };
5357
5361
  const rgbToHsl = (r, g, b) => {
5358
5362
  r /= 255;
@@ -5377,11 +5381,15 @@ const rgbToHsl = (r, g, b) => {
5377
5381
  }
5378
5382
  h *= 60;
5379
5383
  }
5380
- return { h: Math.round(h * 100) / 100, s: Math.round(s * 10000) / 100, l: Math.round(l * 10000) / 100 };
5384
+ return {
5385
+ h: Math.round(h * 100) / 100,
5386
+ s: Math.round(s * 10000) / 100,
5387
+ l: Math.round(l * 10000) / 100,
5388
+ };
5381
5389
  };
5382
5390
  /* ----------------------- 컴포넌트 ----------------------- */
5383
5391
  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, }) => {
5384
- /** 내부 상태 (HSV + alpha%) */
5392
+ /** 내부 HSV + alpha */
5385
5393
  const [h, setH] = useState(211);
5386
5394
  const [s, setS] = useState(100);
5387
5395
  const [v, setV] = useState(50);
@@ -5389,15 +5397,13 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5389
5397
  const [isOpen, setIsOpen] = useState(false);
5390
5398
  const [format, setFormat] = useState('hex');
5391
5399
  const [isCopied, setIsCopied] = useState(false);
5392
- // 하단 인풋(문자열) — 엔터로만 확정
5393
5400
  const [colorInput, setColorInput] = useState('');
5394
5401
  const [alphaInput, setAlphaInput] = useState('100');
5395
- // 모달용 임시 상태 (취소 시 되돌리기)
5396
5402
  const [tempColor, setTempColor] = useState('');
5397
5403
  const pickerRef = useRef(null);
5398
5404
  const areaRef = useRef(null);
5399
5405
  const dragging = useRef(false);
5400
- /** 초기화 */
5406
+ /** 초기값 세팅 */
5401
5407
  useEffect(() => {
5402
5408
  const initial = (value || defaultValue).toUpperCase();
5403
5409
  const rgb = hexToRgb(initial);
@@ -5407,12 +5413,10 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5407
5413
  setS(s);
5408
5414
  setV(v);
5409
5415
  }
5410
- // 인풋 초기 표현
5411
5416
  setColorInput(initial);
5412
5417
  setAlphaInput('100');
5413
- // eslint-disable-next-line react-hooks/exhaustive-deps
5414
5418
  }, []);
5415
- /** 외부 value 변화 시(제어형)만 HSV 반영 */
5419
+ /** 제어형(value) HSV */
5416
5420
  useEffect(() => {
5417
5421
  if (!value)
5418
5422
  return;
@@ -5424,11 +5428,11 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5424
5428
  setV(v);
5425
5429
  }
5426
5430
  }, [value]);
5427
- /** 파생값 */
5431
+ /** RGB/HSL 계산 */
5428
5432
  const rgb = useMemo(() => hsvToRgb(h, s, v), [h, s, v]);
5429
5433
  const hex = useMemo(() => rgbToHex(rgb.r, rgb.g, rgb.b), [rgb]);
5430
5434
  const hsl = useMemo(() => rgbToHsl(rgb.r, rgb.g, rgb.b), [rgb]);
5431
- /** 포맷 출력 문자열 */
5435
+ /** 출력값 formatted */
5432
5436
  const formatted = useMemo(() => {
5433
5437
  const alpha = a / 100;
5434
5438
  switch (format) {
@@ -5440,22 +5444,25 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5440
5444
  default: return hex;
5441
5445
  }
5442
5446
  }, [format, hex, rgb, hsl, a]);
5443
- /** 외부 onChange 알림만 (입력창 동기화는 여기서 하지 않음!) */
5447
+ /** 무한 루프 방지: formatted이 부모 value랑 같으면 onChange 호출하지 않음 */
5444
5448
  useEffect(() => {
5449
+ if (value !== undefined && value.toUpperCase() === formatted.toUpperCase())
5450
+ return;
5445
5451
  onChange?.(formatted);
5446
5452
  // eslint-disable-next-line react-hooks/exhaustive-deps
5447
- }, [formatted]);
5453
+ }, [formatted]); // onChange와 value를 의존성에서 제거
5448
5454
  /** 드롭다운 외부 클릭 닫기 */
5449
5455
  useEffect(() => {
5450
5456
  const handler = (e) => {
5451
- if (pickerRef.current && !pickerRef.current.contains(e.target))
5457
+ if (pickerRef.current && !pickerRef.current.contains(e.target)) {
5452
5458
  setIsOpen(false);
5459
+ }
5453
5460
  };
5454
5461
  if (isOpen && type === 'dropdown')
5455
5462
  document.addEventListener('mousedown', handler);
5456
5463
  return () => document.removeEventListener('mousedown', handler);
5457
5464
  }, [isOpen, type]);
5458
- /** 컬러 영역 (S x V) 마우스/터치 */
5465
+ /** S/V 영역 업데이트 */
5459
5466
  const updateFromArea = (clientX, clientY) => {
5460
5467
  const el = areaRef.current;
5461
5468
  if (!el)
@@ -5482,12 +5489,11 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5482
5489
  const onAlphaChange = useCallback((e) => {
5483
5490
  const newAlpha = parseInt(e.target.value, 10);
5484
5491
  setA(newAlpha);
5485
- setAlphaInput(String(newAlpha)); // 슬라이더 ↔ 인풋 동기화(표시만)
5492
+ setAlphaInput(String(newAlpha));
5486
5493
  }, []);
5487
- /** 컬러 텍스트 인풋: 엔터로만 확정 */
5494
+ /** 컬러 인풋 (엔터 확정) */
5488
5495
  const commitColorInput = useCallback(() => {
5489
5496
  const str = colorInput.trim();
5490
- // HEX (#RRGGBB)
5491
5497
  if (/^#([0-9A-Fa-f]{6})$/.test(str)) {
5492
5498
  const rgb = hexToRgb(str);
5493
5499
  const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
@@ -5497,7 +5503,6 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5497
5503
  setColorInput(str.toUpperCase());
5498
5504
  return;
5499
5505
  }
5500
- // rgb(r,g,b)
5501
5506
  let m = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(str);
5502
5507
  if (m) {
5503
5508
  const r = clamp(parseInt(m[1], 10), 0, 255);
@@ -5510,7 +5515,6 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5510
5515
  setColorInput(`rgb(${r}, ${g}, ${b})`.toUpperCase());
5511
5516
  return;
5512
5517
  }
5513
- // rgba(r,g,b,a)
5514
5518
  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);
5515
5519
  if (m) {
5516
5520
  const r = clamp(parseInt(m[1], 10), 0, 255);
@@ -5526,18 +5530,14 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5526
5530
  setColorInput(`rgba(${r}, ${g}, ${b}, ${alpha})`.toUpperCase());
5527
5531
  return;
5528
5532
  }
5529
- // 잘못된 값 → 원복
5530
5533
  setColorInput(formatted.toUpperCase());
5531
5534
  }, [colorInput, formatted]);
5532
5535
  const onColorKeyDown = (e) => {
5533
5536
  if (e.key === 'Enter')
5534
5537
  commitColorInput();
5535
5538
  };
5536
- const onColorBlur = () => {
5537
- // 요구사항: 엔터로만 반영, 블러 시엔 원복
5538
- setColorInput(formatted.toUpperCase());
5539
- };
5540
- /** 알파 인풋: 엔터로만 확정 (0~100) */
5539
+ const onColorBlur = () => setColorInput(formatted.toUpperCase());
5540
+ /** 알파 인풋 (엔터 확정) */
5541
5541
  const commitAlphaInput = useCallback(() => {
5542
5542
  const n = Number(alphaInput.trim());
5543
5543
  if (!Number.isNaN(n) && n >= 0 && n <= 100) {
@@ -5545,17 +5545,13 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5545
5545
  setAlphaInput(String(Math.round(n)));
5546
5546
  return;
5547
5547
  }
5548
- // 잘못된 값 → 원복
5549
5548
  setAlphaInput(String(a));
5550
5549
  }, [alphaInput, a]);
5551
5550
  const onAlphaInputKeyDown = (e) => {
5552
5551
  if (e.key === 'Enter')
5553
5552
  commitAlphaInput();
5554
5553
  };
5555
- const onAlphaInputBlur = () => {
5556
- // 엔터로만 반영, 블러 시 원복
5557
- setAlphaInput(String(a));
5558
- };
5554
+ const onAlphaInputBlur = () => setAlphaInput(String(a));
5559
5555
  /** 복사 */
5560
5556
  const onCopy = useCallback(async () => {
5561
5557
  try {
@@ -5579,18 +5575,15 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5579
5575
  setS(hsv.s);
5580
5576
  setV(hsv.v);
5581
5577
  }
5582
- else {
5583
- console.log('EyeDropper API is not supported');
5584
- }
5585
5578
  }
5586
5579
  catch (e) {
5587
5580
  console.error(e);
5588
5581
  }
5589
5582
  }, []);
5590
- /** 모달 열기/적용/취소 (deps에서 formatted 제거 → 재생성 방지) */
5583
+ /** modal open(중요: formatted deps 제거) */
5591
5584
  const handleModalOpen = useCallback(() => {
5592
5585
  if (type === 'modal')
5593
- setTempColor(hex); // 현재 HEX 저장
5586
+ setTempColor(hex);
5594
5587
  setIsOpen(true);
5595
5588
  }, [type, hex]);
5596
5589
  const handleModalApply = useCallback(() => {
@@ -5603,53 +5596,55 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5603
5596
  if (type === 'modal') {
5604
5597
  const rgb = hexToRgb(tempColor);
5605
5598
  if (rgb) {
5606
- const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);
5607
- setH(h);
5608
- setS(s);
5609
- setV(v);
5599
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
5600
+ setH(hsv.h);
5601
+ setS(hsv.s);
5602
+ setV(hsv.v);
5610
5603
  }
5611
5604
  onCancel?.();
5612
5605
  setIsOpen(false);
5613
5606
  }
5614
5607
  }, [type, tempColor, onCancel]);
5615
- /** 배경 스타일들 */
5608
+ /** 스타일 */
5616
5609
  const hueTrackStyle = useMemo(() => ({
5617
5610
  background: `linear-gradient(to right,
5618
- hsl(0,100%,50%),
5619
- hsl(60,100%,50%),
5620
- hsl(120,100%,50%),
5621
- hsl(180,100%,50%),
5622
- hsl(240,100%,50%),
5623
- hsl(300,100%,50%),
5624
- hsl(360,100%,50%))`,
5611
+ hsl(0,100%,50%),
5612
+ hsl(60,100%,50%),
5613
+ hsl(120,100%,50%),
5614
+ hsl(180,100%,50%),
5615
+ hsl(240,100%,50%),
5616
+ hsl(300,100%,50%),
5617
+ hsl(360,100%,50%))`,
5625
5618
  }), []);
5626
- // 상단 좌→우: 채도 증가 / 상단→하단: 밝기 감소
5627
5619
  const areaBackground = useMemo(() => ({
5628
5620
  backgroundImage: `
5629
- linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)),
5630
- linear-gradient(to right, #ffffff, hsl(${h}, 100%, 50%))
5631
- `,
5621
+ linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)),
5622
+ linear-gradient(to right, #ffffff, hsl(${h}, 100%, 50%))
5623
+ `,
5632
5624
  }), [h]);
5633
- // ✔ 고정 흑백 알파 트랙 (색상 무관)
5634
5625
  const alphaTrackStyle = useMemo(() => ({
5635
5626
  backgroundImage: `
5636
- linear-gradient(45deg, var(--db-border-base) 25%, transparent 25%),
5637
- linear-gradient(-45deg, var(--db-border-base) 25%, transparent 25%),
5638
- linear-gradient(45deg, transparent 75%, var(--db-border-base) 75%),
5639
- linear-gradient(-45deg, transparent 75%, var(--db-border-base) 75%),
5640
- linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))
5641
- `,
5627
+ linear-gradient(45deg, var(--db-border-base) 25%, transparent 25%),
5628
+ linear-gradient(-45deg, var(--db-border-base) 25%, transparent 25%),
5629
+ linear-gradient(45deg, transparent 75%, var(--db-border-base) 75%),
5630
+ linear-gradient(-45deg, transparent 75%, var(--db-border-base) 75%),
5631
+ linear-gradient(to right, rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0), rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1))
5632
+ `,
5642
5633
  backgroundSize: '8px 8px,8px 8px,8px 8px,8px 8px,100% 100%',
5643
5634
  backgroundPosition: '0 0,0 4px,4px -4px,-4px 0,0 0',
5644
5635
  backgroundColor: 'var(--db-surface-base)',
5645
- }), []);
5636
+ }), [rgb]);
5646
5637
  const classes = clsx('designbase-color-picker', `designbase-color-picker--${size}`, `designbase-color-picker--${position}`, {
5647
5638
  'designbase-color-picker--disabled': disabled,
5648
5639
  'designbase-color-picker--readonly': readonly,
5649
5640
  'designbase-color-picker--open': isOpen,
5650
5641
  'designbase-color-picker--no-input': !showInput,
5651
5642
  }, className);
5652
- const Trigger = (jsxs("div", { className: "designbase-color-picker__trigger", onClick: () => !disabled && !readonly && (type === 'modal' ? handleModalOpen() : setIsOpen(v => !v)), 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 }) })] }));
5643
+ const Trigger = (jsxs("div", { className: "designbase-color-picker__trigger", onClick: () => !disabled && !readonly && (type === 'modal' ? handleModalOpen() : setIsOpen(v => !v)), children: [jsx("div", { className: "designbase-color-picker__color-display", children: jsx("div", { className: "designbase-color-picker__color-box", style: {
5644
+ backgroundColor: showAlpha
5645
+ ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a / 100})`
5646
+ : hex
5647
+ } }) }), 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 }) })] }));
5653
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, { variant: "tertiary", size: "m", iconOnly: true, onClick: onEyedrop, "aria-label": "Eyedropper tool", children: jsx(EyedropperIcon, {}) }), jsxs("div", { className: "designbase-color-picker__slider-container", children: [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: [
5654
5649
  { label: 'HEX', value: 'hex' },
5655
5650
  { label: 'RGB', value: 'rgb' },
@@ -5657,7 +5652,7 @@ const ColorPicker = ({ size = 'm', type = 'dropdown', position = 'bottom-left',
5657
5652
  { label: 'HSL', value: 'hsl' },
5658
5653
  { label: 'HSLA', value: 'hsla' },
5659
5654
  ], size: "s", position: "top" })), jsx(Input, { type: "text", value: colorInput, onChange: setColorInput, onKeyDown: onColorKeyDown, onBlur: onColorBlur, placeholder: "#000000", size: "s", className: "designbase-color-picker__value-input" }), showAlpha && (jsxs("div", { className: "designbase-color-picker__alpha-input-wrap", children: [jsx(Input, { type: "text", inputMode: "numeric", value: alphaInput, onChange: (value) => setAlphaInput(value.replace(/[^\d]/g, '').slice(0, 3)), onKeyDown: onAlphaInputKeyDown, onBlur: onAlphaInputBlur, placeholder: "100", size: "s", className: "designbase-color-picker__alpha-input", "aria-label": "Alpha percent" }), jsx("span", { className: "designbase-color-picker__alpha-suffix", children: "%" })] })), showCopyButton && (jsx(Button, { variant: "tertiary", size: "m", iconOnly: true, onClick: onCopy, "aria-label": "Copy color value", children: isCopied ? jsx(DoneIcon$1, {}) : jsx(CopyIcon, {}) }))] })] }));
5660
- 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: [jsx(ModalBody, { children: Selector }), jsx(ModalFooter, { children: jsxs("div", { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' }, children: [jsx(Button, { variant: "tertiary", size: "s", onClick: handleModalCancel, children: "\uCDE8\uC18C" }), jsx(Button, { variant: "primary", size: "s", onClick: handleModalApply, children: "\uC801\uC6A9" })] }) })] }))] }));
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: [jsx(ModalBody, { children: Selector }), jsx(ModalFooter, { children: jsxs("div", { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' }, children: [jsx(Button, { variant: "tertiary", size: "m", onClick: handleModalCancel, children: "\uCDE8\uC18C" }), jsx(Button, { variant: "primary", size: "m", onClick: handleModalApply, children: "\uC801\uC6A9" })] }) })] }))] }));
5661
5656
  };
5662
5657
  ColorPicker.displayName = 'ColorPicker';
5663
5658