@hilum/ui 3.8.4 → 3.8.5

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/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Key Facts
6
6
 
7
7
  - Package name: `@hilum/ui`
8
- - Package version: 3.8.4
8
+ - Package version: 3.8.5
9
9
  - Component modules: 104
10
10
  - Atom components: 73
11
11
  - Molecule components: 31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.8.4",
3
+ "version": "3.8.5",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -392,6 +392,21 @@ function parseColor(input: string): { r: number; g: number; b: number; a: number
392
392
  return null;
393
393
  }
394
394
 
395
+ function colorsRepresentSameValue(left: string, right: string): boolean {
396
+ if (left.trim().toLowerCase() === right.trim().toLowerCase()) return true;
397
+
398
+ const leftColor = parseColor(left);
399
+ const rightColor = parseColor(right);
400
+ if (!leftColor || !rightColor) return false;
401
+
402
+ return (
403
+ Math.abs(leftColor.r - rightColor.r) < 0.5 &&
404
+ Math.abs(leftColor.g - rightColor.g) < 0.5 &&
405
+ Math.abs(leftColor.b - rightColor.b) < 0.5 &&
406
+ Math.abs(leftColor.a - rightColor.a) < 0.5 / 255
407
+ );
408
+ }
409
+
395
410
  function buildParsed(h: number, s: number, v: number, a: number): ParsedColor {
396
411
  const { r, g, b } = hsvToRgb(h, s, v);
397
412
  const hsl = rgbToHsl(r, g, b);
@@ -463,7 +478,7 @@ interface SaturationSquareProps {
463
478
  function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
464
479
  const ref = useRef<HTMLDivElement>(null);
465
480
  const dragging = useRef(false);
466
- const hasMoved = useRef(false);
481
+ const [isDragging, setIsDragging] = useState(false);
467
482
  const [focused, setFocused] = useState(false);
468
483
  const [hovered, setHovered] = useState(false);
469
484
  const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null);
@@ -494,7 +509,7 @@ function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
494
509
  if (e.pointerType === "mouse" && e.button !== 0) return;
495
510
  e.preventDefault();
496
511
  dragging.current = true;
497
- hasMoved.current = false;
512
+ setIsDragging(true);
498
513
  (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
499
514
  updateFromPointer(e.clientX, e.clientY);
500
515
  },
@@ -505,7 +520,6 @@ function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
505
520
  (e: React.PointerEvent<HTMLDivElement>) => {
506
521
  updateCursorPos(e.clientX, e.clientY);
507
522
  if (!dragging.current) return;
508
- hasMoved.current = true;
509
523
  updateFromPointer(e.clientX, e.clientY);
510
524
  },
511
525
  [updateFromPointer, updateCursorPos],
@@ -513,7 +527,7 @@ function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
513
527
 
514
528
  const onPointerUp = useCallback(() => {
515
529
  dragging.current = false;
516
- hasMoved.current = false;
530
+ setIsDragging(false);
517
531
  }, []);
518
532
 
519
533
  const onKeyDown = useCallback(
@@ -556,6 +570,8 @@ function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
556
570
  onPointerDown={onPointerDown}
557
571
  onPointerMove={onPointerMove}
558
572
  onPointerUp={onPointerUp}
573
+ onPointerCancel={onPointerUp}
574
+ onLostPointerCapture={onPointerUp}
559
575
  onKeyDown={onKeyDown}
560
576
  className={cn("relative w-full select-none touch-none cursor-none outline-none", shape.bg)}
561
577
  style={{
@@ -572,25 +588,25 @@ function SaturationSquare({ h, s, v, onChange }: SaturationSquareProps) {
572
588
  background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${h}, 100%, 50%))`,
573
589
  }}
574
590
  />
575
- <motion.div
591
+ <div
592
+ data-slot="color-picker-thumb"
593
+ aria-hidden="true"
576
594
  className="absolute pointer-events-none rounded-full"
577
- initial={false}
578
- animate={{
595
+ style={{
579
596
  left: `${s * 100}%`,
580
597
  top: `${(1 - v) * 100}%`,
581
598
  width: 18,
582
599
  height: 18,
583
- }}
584
- transition={{ duration: 0 }}
585
- style={{
586
600
  transform: "translate(-50%, -50%)",
587
601
  border: "1px solid white",
588
602
  boxShadow: "0 0 0 1px rgba(0,0,0,1)",
589
603
  backgroundColor: thumbColor,
590
604
  }}
591
605
  />
592
- {hovered && !dragging.current && cursorPos && (
606
+ {hovered && !isDragging && cursorPos && (
593
607
  <div
608
+ data-slot="color-picker-hover"
609
+ aria-hidden="true"
594
610
  className="absolute pointer-events-none rounded-full"
595
611
  style={{
596
612
  left: `${cursorPos.x}%`,
@@ -1338,7 +1354,11 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
1338
1354
  if (!isControlled) return;
1339
1355
  const emitted = lastEmittedRef.current;
1340
1356
  const cur = value as string;
1341
- if (cur === emitted) return;
1357
+ // Controlled consumers commonly normalize emitted hex strings (for
1358
+ // example, Pappery uppercases them). Treat a formatting-only round trip
1359
+ // as an acknowledgement of our last emission so the precise in-flight
1360
+ // HSV position is not replaced with RGB-quantized coordinates mid-drag.
1361
+ if (cur === emitted || colorsRepresentSameValue(cur, emitted)) return;
1342
1362
  const p = parseColor(cur);
1343
1363
  if (!p) return;
1344
1364
  oklchHueRef.current = null;