@designbasekorea/ui 0.2.23 → 0.2.26

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