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