@zag-js/color-picker 0.29.0 → 0.30.0

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.mjs CHANGED
@@ -17,8 +17,11 @@ var anatomy = createAnatomy("color-picker", [
17
17
  "transparencyGrid",
18
18
  "swatchGroup",
19
19
  "swatchTrigger",
20
+ "swatchIndicator",
20
21
  "swatch",
21
- "eyeDropperTrigger"
22
+ "eyeDropperTrigger",
23
+ "formatTrigger",
24
+ "formatSelect"
22
25
  ]);
23
26
  var parts = anatomy.build();
24
27
 
@@ -49,6 +52,7 @@ var dom = createScope({
49
52
  getTriggerId: (ctx) => ctx.ids?.trigger ?? `color-picker:${ctx.id}:trigger`,
50
53
  getContentId: (ctx) => ctx.ids?.content ?? `color-picker:${ctx.id}:content`,
51
54
  getPositionerId: (ctx) => `color-picker:${ctx.id}:positioner`,
55
+ getFormatSelectId: (ctx) => `color-picker:${ctx.id}:format-select`,
52
56
  getAreaId: (ctx) => ctx.ids?.area ?? `color-picker:${ctx.id}:area`,
53
57
  getAreaGradientId: (ctx) => ctx.ids?.areaGradient ?? `color-picker:${ctx.id}:area-gradient`,
54
58
  getAreaThumbId: (ctx) => ctx.ids?.areaThumb ?? `color-picker:${ctx.id}:area-thumb`,
@@ -63,6 +67,7 @@ var dom = createScope({
63
67
  ...queryAll(dom.getControlEl(ctx), `input[data-channel="${channel}"]`)
64
68
  ];
65
69
  },
70
+ getFormatSelectEl: (ctx) => dom.getById(ctx, dom.getFormatSelectId(ctx)),
66
71
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
67
72
  getAreaEl: (ctx) => dom.getById(ctx, dom.getAreaId(ctx)),
68
73
  getAreaValueFromPoint(ctx, point) {
@@ -94,10 +99,7 @@ var dom = createScope({
94
99
  getFirstFocusableEl: (ctx) => getFirstFocusable(dom.getContentEl(ctx), "if-empty"),
95
100
  getInitialFocusEl: (ctx) => {
96
101
  let el = runIfFn(ctx.initialFocusEl);
97
- if (!el && ctx.autoFocus)
98
- el = dom.getFirstFocusableEl(ctx);
99
- if (!el)
100
- el = dom.getContentEl(ctx);
102
+ el = dom.getFirstFocusableEl(ctx) ?? dom.getContentEl(ctx);
101
103
  return el;
102
104
  }
103
105
  });
@@ -142,7 +144,7 @@ function getChannelValue(color, channel) {
142
144
  case "hue":
143
145
  return isHSL ? color.toFormat("hsla").getChannelValue("hue").toString() : color.toFormat("hsba").getChannelValue("hue").toString();
144
146
  case "saturation":
145
- return isHSL ? color.toFormat("hsl").getChannelValue("saturation").toString() : color.toFormat("hsb").getChannelValue("saturation").toString();
147
+ return isHSL ? color.toFormat("hsla").getChannelValue("saturation").toString() : color.toFormat("hsba").getChannelValue("saturation").toString();
146
148
  case "lightness":
147
149
  return color.toFormat("hsla").getChannelValue("lightness").toString();
148
150
  case "brightness":
@@ -171,13 +173,13 @@ function getChannelRange(color, channel) {
171
173
  case "hue":
172
174
  case "saturation":
173
175
  case "lightness":
174
- return color.toFormat("hsl").getChannelRange(channel);
176
+ return color.toFormat("hsla").getChannelRange(channel);
175
177
  case "brightness":
176
- return color.toFormat("hsb").getChannelRange(channel);
178
+ return color.toFormat("hsba").getChannelRange(channel);
177
179
  case "red":
178
180
  case "green":
179
181
  case "blue":
180
- return color.toFormat("rgb").getChannelRange(channel);
182
+ return color.toFormat("rgba").getChannelRange(channel);
181
183
  default:
182
184
  return color.getChannelRange(channel);
183
185
  }
@@ -224,13 +226,15 @@ var getSliderBackground = (props) => {
224
226
  // src/color-picker.connect.ts
225
227
  function connect(state, send, normalize) {
226
228
  const value = state.context.value;
229
+ const areaValue = state.context.areaValue;
227
230
  const valueAsString = state.context.valueAsString;
228
231
  const isDisabled = state.context.isDisabled;
229
232
  const isInteractive = state.context.isInteractive;
230
233
  const isDragging = state.hasTag("dragging");
231
234
  const isOpen = state.hasTag("open");
235
+ const isFocused = state.hasTag("focused");
232
236
  const getAreaChannels = (props) => {
233
- const channels = value.getChannels();
237
+ const channels = areaValue.getChannels();
234
238
  return {
235
239
  xChannel: props.xChannel ?? channels[1],
236
240
  yChannel: props.yChannel ?? channels[2]
@@ -241,11 +245,26 @@ function connect(state, send, normalize) {
241
245
  ...state.context.positioning,
242
246
  placement: currentPlacement
243
247
  });
248
+ function getSwatchTriggerState(props) {
249
+ const color = normalizeColor(props.value).toFormat(state.context.format);
250
+ return {
251
+ value: color,
252
+ valueAsString: color.toString("hex"),
253
+ isChecked: color.isEqual(value),
254
+ isDisabled: props.disabled || !isInteractive
255
+ };
256
+ }
244
257
  return {
245
258
  isDragging,
246
259
  isOpen,
247
260
  valueAsString,
248
261
  value,
262
+ open() {
263
+ send({ type: "OPEN" });
264
+ },
265
+ close() {
266
+ send({ type: "CLOSE" });
267
+ },
249
268
  setValue(value2) {
250
269
  send({ type: "VALUE.SET", value: normalizeColor(value2), src: "set-color" });
251
270
  },
@@ -256,7 +275,7 @@ function connect(state, send, normalize) {
256
275
  const color = value.withChannelValue(channel, channelValue);
257
276
  send({ type: "VALUE.SET", value: color, src: "set-channel" });
258
277
  },
259
- format: value.outputFormat ?? value.getFormat(),
278
+ format: state.context.format,
260
279
  setFormat(format) {
261
280
  const formatValue = value.toFormat(format);
262
281
  send({ type: "VALUE.SET", value: formatValue, src: "set-format" });
@@ -283,6 +302,7 @@ function connect(state, send, normalize) {
283
302
  htmlFor: dom.getHiddenInputId(state.context),
284
303
  "data-disabled": dataAttr(isDisabled),
285
304
  "data-readonly": dataAttr(state.context.readOnly),
305
+ "data-focus": dataAttr(isFocused),
286
306
  onClick(event) {
287
307
  event.preventDefault();
288
308
  const inputEl = query(dom.getControlEl(state.context), "[data-channel=hex]");
@@ -294,21 +314,35 @@ function connect(state, send, normalize) {
294
314
  id: dom.getControlId(state.context),
295
315
  dir: state.context.dir,
296
316
  "data-disabled": dataAttr(isDisabled),
297
- "data-readonly": dataAttr(state.context.readOnly)
317
+ "data-readonly": dataAttr(state.context.readOnly),
318
+ "data-state": isOpen ? "open" : "closed",
319
+ "data-focus": dataAttr(isFocused)
298
320
  }),
299
321
  triggerProps: normalize.button({
300
322
  ...parts.trigger.attrs,
301
323
  id: dom.getTriggerId(state.context),
302
324
  dir: state.context.dir,
325
+ disabled: isDisabled,
326
+ "aria-label": `select color. current color is ${valueAsString}`,
303
327
  "aria-controls": dom.getContentId(state.context),
304
328
  "aria-labelledby": dom.getLabelId(state.context),
305
329
  "data-disabled": dataAttr(isDisabled),
306
330
  "data-readonly": dataAttr(state.context.readOnly),
307
331
  "data-placement": currentPlacement,
332
+ "aria-expanded": dataAttr(isOpen),
333
+ "data-state": isOpen ? "open" : "closed",
334
+ "data-focus": dataAttr(isFocused),
308
335
  type: "button",
309
336
  onClick() {
337
+ if (!isInteractive)
338
+ return;
310
339
  send({ type: "TRIGGER.CLICK" });
311
340
  },
341
+ onBlur() {
342
+ if (!isInteractive)
343
+ return;
344
+ send({ type: "TRIGGER.BLUR" });
345
+ },
312
346
  style: {
313
347
  position: "relative"
314
348
  }
@@ -324,11 +358,12 @@ function connect(state, send, normalize) {
324
358
  id: dom.getContentId(state.context),
325
359
  dir: state.context.dir,
326
360
  "data-placement": currentPlacement,
361
+ "data-state": isOpen ? "open" : "closed",
327
362
  hidden: !isOpen
328
363
  }),
329
364
  getAreaProps(props = {}) {
330
365
  const { xChannel, yChannel } = getAreaChannels(props);
331
- const { areaStyles } = getColorAreaGradient(value, {
366
+ const { areaStyles } = getColorAreaGradient(areaValue, {
332
367
  xChannel,
333
368
  yChannel,
334
369
  dir: state.context.dir
@@ -358,7 +393,7 @@ function connect(state, send, normalize) {
358
393
  },
359
394
  getAreaBackgroundProps(props = {}) {
360
395
  const { xChannel, yChannel } = getAreaChannels(props);
361
- const { areaGradientStyles } = getColorAreaGradient(value, {
396
+ const { areaGradientStyles } = getColorAreaGradient(areaValue, {
362
397
  xChannel,
363
398
  yChannel,
364
399
  dir: state.context.dir
@@ -377,25 +412,35 @@ function connect(state, send, normalize) {
377
412
  getAreaThumbProps(props = {}) {
378
413
  const { xChannel, yChannel } = getAreaChannels(props);
379
414
  const channel = { xChannel, yChannel };
380
- const x = value.getChannelValuePercent(xChannel);
381
- const y = 1 - value.getChannelValuePercent(yChannel);
415
+ const xPercent = areaValue.getChannelValuePercent(xChannel);
416
+ const yPercent = 1 - areaValue.getChannelValuePercent(yChannel);
417
+ const xValue = areaValue.getChannelValue(xChannel);
418
+ const yValue = areaValue.getChannelValue(yChannel);
382
419
  return normalize.element({
383
420
  ...parts.areaThumb.attrs,
384
421
  id: dom.getAreaThumbId(state.context),
385
422
  dir: state.context.dir,
386
423
  tabIndex: isDisabled ? void 0 : 0,
387
424
  "data-disabled": dataAttr(isDisabled),
388
- role: "presentation",
425
+ role: "slider",
426
+ "aria-valuemin": 0,
427
+ "aria-valuemax": 100,
428
+ "aria-valuenow": xValue,
429
+ "aria-label": `${xChannel} and ${yChannel}`,
430
+ "aria-roledescription": "2d slider",
431
+ "aria-valuetext": `${xChannel} ${xValue}, ${yChannel} ${yValue}`,
389
432
  style: {
390
433
  position: "absolute",
391
- left: `${x * 100}%`,
392
- top: `${y * 100}%`,
434
+ left: `${xPercent * 100}%`,
435
+ top: `${yPercent * 100}%`,
393
436
  transform: "translate(-50%, -50%)",
394
437
  touchAction: "none",
395
438
  forcedColorAdjust: "none",
396
- background: value.withChannelValue("alpha", 1).toString("css")
439
+ background: areaValue.withChannelValue("alpha", 1).toString("css")
397
440
  },
398
441
  onFocus() {
442
+ if (!isInteractive)
443
+ return;
399
444
  send({ type: "AREA.FOCUS", id: "area", channel });
400
445
  },
401
446
  onKeyDown(event) {
@@ -489,15 +534,15 @@ function connect(state, send, normalize) {
489
534
  orientation,
490
535
  channel,
491
536
  dir: state.context.dir,
492
- value
537
+ value: areaValue
493
538
  })
494
539
  }
495
540
  });
496
541
  },
497
542
  getChannelSliderThumbProps(props) {
498
543
  const { orientation = "horizontal", channel } = props;
499
- const { minValue, maxValue, step: stepValue } = value.getChannelRange(channel);
500
- const channelValue = value.getChannelValue(channel);
544
+ const { minValue, maxValue, step: stepValue } = areaValue.getChannelRange(channel);
545
+ const channelValue = areaValue.getChannelValue(channel);
501
546
  const offset = (channelValue - minValue) / (maxValue - minValue);
502
547
  const placementStyles = orientation === "horizontal" ? { left: `${offset * 100}%`, top: "50%" } : { top: `${offset * 100}%`, left: "50%" };
503
548
  return normalize.element({
@@ -514,10 +559,11 @@ function connect(state, send, normalize) {
514
559
  "aria-valuemax": maxValue,
515
560
  "aria-valuemin": minValue,
516
561
  "aria-valuenow": channelValue,
562
+ "aria-valuetext": `${channel} ${channelValue}`,
517
563
  style: {
518
564
  forcedColorAdjust: "none",
519
565
  position: "absolute",
520
- background: getChannelDisplayColor(value, channel).toString("css"),
566
+ background: getChannelDisplayColor(areaValue, channel).toString("css"),
521
567
  ...placementStyles
522
568
  },
523
569
  onFocus() {
@@ -576,6 +622,8 @@ function connect(state, send, normalize) {
576
622
  type: isTextField ? "text" : "number",
577
623
  "data-channel": channel,
578
624
  "aria-label": channel,
625
+ spellCheck: false,
626
+ autoComplete: "off",
579
627
  disabled: isDisabled,
580
628
  "data-disabled": dataAttr(isDisabled),
581
629
  readOnly: state.context.readOnly,
@@ -584,7 +632,7 @@ function connect(state, send, normalize) {
584
632
  max: range?.maxValue,
585
633
  step: range?.step,
586
634
  onBeforeInput(event) {
587
- if (isTextField)
635
+ if (isTextField || !isInteractive)
588
636
  return;
589
637
  const value2 = event.currentTarget.value;
590
638
  if (value2.match(/[^0-9.]/g)) {
@@ -592,14 +640,20 @@ function connect(state, send, normalize) {
592
640
  }
593
641
  },
594
642
  onFocus(event) {
643
+ if (!isInteractive)
644
+ return;
595
645
  send({ type: "CHANNEL_INPUT.FOCUS", channel });
596
646
  event.target.select();
597
647
  },
598
648
  onBlur(event) {
649
+ if (!isInteractive)
650
+ return;
599
651
  const value2 = isTextField ? event.currentTarget.value : event.currentTarget.valueAsNumber;
600
652
  send({ type: "CHANNEL_INPUT.BLUR", channel, value: value2, isTextField });
601
653
  },
602
654
  onKeyDown(event) {
655
+ if (!isInteractive)
656
+ return;
603
657
  if (event.key === "Enter") {
604
658
  const value2 = isTextField ? event.currentTarget.value : event.currentTarget.valueAsNumber;
605
659
  send({ type: "CHANNEL_INPUT.CHANGE", channel, value: value2, isTextField });
@@ -638,42 +692,87 @@ function connect(state, send, normalize) {
638
692
  ...parts.swatchGroup.attrs,
639
693
  role: "group"
640
694
  }),
695
+ getSwatchTriggerState,
641
696
  getSwatchTriggerProps(props) {
642
- const { value: valueProp } = props;
643
- const color = normalizeColor(valueProp).toFormat(value.getFormat());
697
+ const triggerState = getSwatchTriggerState(props);
644
698
  return normalize.button({
645
699
  ...parts.swatchTrigger.attrs,
646
- disabled: isDisabled,
700
+ disabled: triggerState.isDisabled,
647
701
  dir: state.context.dir,
648
702
  type: "button",
649
- "data-value": color.toString("hex"),
703
+ "aria-label": `select ${triggerState.valueAsString} as the color`,
704
+ "data-state": triggerState.isChecked ? "checked" : "unchecked",
705
+ "data-value": triggerState.valueAsString,
706
+ "data-disabled": dataAttr(triggerState.isDisabled),
650
707
  onClick() {
651
- if (!isInteractive)
708
+ if (triggerState.isDisabled)
652
709
  return;
653
- send({ type: "VALUE.SET", value: color });
710
+ send({ type: "SWATCH_TRIGGER.CLICK", value: triggerState.value });
654
711
  },
655
712
  style: {
656
713
  position: "relative"
657
714
  }
658
715
  });
659
716
  },
717
+ getSwatchIndicatorProps(props) {
718
+ const triggerState = getSwatchTriggerState(props);
719
+ return normalize.element({
720
+ ...parts.swatchIndicator.attrs,
721
+ dir: state.context.dir,
722
+ "aria-hidden": true,
723
+ "data-state": triggerState.isChecked ? "open" : "closed"
724
+ });
725
+ },
660
726
  getSwatchProps(props) {
661
- const { value: valueProp, respectAlpha = true } = props;
662
- const colorValue = normalizeColor(valueProp);
663
- const color = colorValue.toFormat(value.getFormat());
727
+ const { respectAlpha = true } = props;
728
+ const triggerState = getSwatchTriggerState(props);
664
729
  return normalize.element({
665
730
  ...parts.swatch.attrs,
666
731
  dir: state.context.dir,
667
- "data-state": colorValue.isEqual(value) ? "selected" : "unselected",
668
- "data-value": color.toString("hex"),
732
+ "data-state": triggerState.isChecked ? "checked" : "unchecked",
733
+ "data-value": triggerState.valueAsString,
669
734
  style: {
670
735
  position: "relative",
671
- background: color.toString(respectAlpha ? "css" : "hex")
736
+ background: triggerState.value.toString(respectAlpha ? "css" : "hex")
672
737
  }
673
738
  });
674
- }
739
+ },
740
+ formatTriggerProps: normalize.button({
741
+ ...parts.formatTrigger.attrs,
742
+ dir: state.context.dir,
743
+ type: "button",
744
+ "aria-label": `change color format to ${getNextFormat(state.context.format)}`,
745
+ onClick(event) {
746
+ if (event.currentTarget.disabled)
747
+ return;
748
+ const nextFormat = getNextFormat(state.context.format);
749
+ send({ type: "FORMAT.SET", format: nextFormat, src: "format-trigger" });
750
+ }
751
+ }),
752
+ formatSelectProps: normalize.select({
753
+ ...parts.formatSelect.attrs,
754
+ "aria-label": "change color format",
755
+ dir: state.context.dir,
756
+ defaultValue: state.context.format,
757
+ disabled: isDisabled,
758
+ onChange(event) {
759
+ const format = assertFormat(event.currentTarget.value);
760
+ send({ type: "FORMAT.SET", format, src: "format-select" });
761
+ }
762
+ })
675
763
  };
676
764
  }
765
+ var formats = ["hsba", "hsla", "rgba"];
766
+ var formatRegex = new RegExp(`^(${formats.join("|")})$`);
767
+ function getNextFormat(format) {
768
+ const index = formats.indexOf(format);
769
+ return formats[index + 1] ?? formats[0];
770
+ }
771
+ function assertFormat(format) {
772
+ if (formatRegex.test(format))
773
+ return format;
774
+ throw new Error(`Unsupported color format: ${format}`);
775
+ }
677
776
 
678
777
  // src/color-picker.machine.ts
679
778
  import { parseColor as parseColor4 } from "@zag-js/color-utils";
@@ -688,13 +787,8 @@ import { compact, tryCatch } from "@zag-js/utils";
688
787
 
689
788
  // src/color-picker.parse.ts
690
789
  import { parseColor as parseColor3 } from "@zag-js/color-utils";
691
- var parse = (color) => {
692
- const value = parseColor3(color);
693
- const colorFormat = value.getFormat();
694
- const lockedFormat = colorFormat === "hsla" ? "hsla" : "hsba";
695
- const result = value.toFormat(lockedFormat);
696
- result.outputFormat = colorFormat;
697
- return result;
790
+ var parse = (colorString) => {
791
+ return parseColor3(colorString);
698
792
  };
699
793
 
700
794
  // src/color-picker.machine.ts
@@ -707,13 +801,14 @@ function machine(userContext) {
707
801
  context: {
708
802
  dir: "ltr",
709
803
  value: parse("#000000"),
804
+ format: "rgba",
710
805
  disabled: false,
806
+ closeOnSelect: false,
711
807
  ...ctx,
712
808
  activeId: null,
713
809
  activeChannel: null,
714
810
  activeOrientation: null,
715
811
  fieldsetDisabled: false,
716
- autoFocus: true,
717
812
  positioning: {
718
813
  ...ctx.positioning,
719
814
  placement: "bottom"
@@ -723,22 +818,30 @@ function machine(userContext) {
723
818
  isRtl: (ctx2) => ctx2.dir === "rtl",
724
819
  isDisabled: (ctx2) => !!ctx2.disabled || ctx2.fieldsetDisabled,
725
820
  isInteractive: (ctx2) => !(ctx2.isDisabled || ctx2.readOnly),
726
- valueAsString: (ctx2) => {
727
- const color = ctx2.value.outputFormat ? ctx2.value.toFormat(ctx2.value.outputFormat) : ctx2.value;
728
- return color.toString("css");
821
+ valueAsString: (ctx2) => ctx2.value.toString(ctx2.format),
822
+ areaValue: (ctx2) => {
823
+ const format = ctx2.format.startsWith("hsl") ? "hsla" : "hsba";
824
+ return ctx2.value.toFormat(format);
729
825
  }
730
826
  },
731
827
  activities: ["trackFormControl"],
732
828
  watch: {
733
829
  value: ["syncInputElements"],
830
+ format: ["syncFormatSelectElement"],
734
831
  open: ["toggleVisibility"]
735
832
  },
736
833
  on: {
737
834
  "VALUE.SET": {
738
835
  actions: ["setValue"]
739
836
  },
837
+ "FORMAT.SET": {
838
+ actions: ["setFormat"]
839
+ },
740
840
  "CHANNEL_INPUT.CHANGE": {
741
841
  actions: ["setChannelColorFromInput"]
842
+ },
843
+ "EYEDROPPER.CLICK": {
844
+ actions: ["openEyeDropper"]
742
845
  }
743
846
  },
744
847
  states: {
@@ -776,6 +879,9 @@ function machine(userContext) {
776
879
  "CHANNEL_INPUT.BLUR": {
777
880
  target: "idle",
778
881
  actions: ["setChannelColorFromInput"]
882
+ },
883
+ "TRIGGER.BLUR": {
884
+ target: "idle"
779
885
  }
780
886
  }
781
887
  },
@@ -787,9 +893,6 @@ function machine(userContext) {
787
893
  target: "idle",
788
894
  actions: ["invokeOnClose"]
789
895
  },
790
- "EYEDROPPER.CLICK": {
791
- actions: ["openEyeDropper"]
792
- },
793
896
  "AREA.POINTER_DOWN": {
794
897
  target: "open:dragging",
795
898
  actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"]
@@ -863,7 +966,17 @@ function machine(userContext) {
863
966
  CLOSE: {
864
967
  target: "idle",
865
968
  actions: ["invokeOnClose"]
866
- }
969
+ },
970
+ "SWATCH_TRIGGER.CLICK": [
971
+ {
972
+ guard: "closeOnSelect",
973
+ target: "focused",
974
+ actions: ["setValue", "setReturnFocus", "invokeOnClose"]
975
+ },
976
+ {
977
+ actions: ["setValue"]
978
+ }
979
+ ]
867
980
  }
868
981
  },
869
982
  "open:dragging": {
@@ -906,7 +1019,8 @@ function machine(userContext) {
906
1019
  },
907
1020
  {
908
1021
  guards: {
909
- isTargetFocusable: (_ctx, evt) => evt.restoreFocus
1022
+ isTargetFocusable: (_ctx, evt) => evt.restoreFocus,
1023
+ closeOnSelect: (ctx2) => !!ctx2.closeOnSelect
910
1024
  },
911
1025
  activities: {
912
1026
  trackPositioning(ctx2) {
@@ -1001,9 +1115,9 @@ function machine(userContext) {
1001
1115
  const percent = dom.getAreaValueFromPoint(ctx2, evt.point);
1002
1116
  if (!percent)
1003
1117
  return;
1004
- const xValue = ctx2.value.getChannelPercentValue(xChannel, percent.x);
1005
- const yValue = ctx2.value.getChannelPercentValue(yChannel, 1 - percent.y);
1006
- const color = ctx2.value.withChannelValue(xChannel, xValue).withChannelValue(yChannel, yValue);
1118
+ const xValue = ctx2.areaValue.getChannelPercentValue(xChannel, percent.x);
1119
+ const yValue = ctx2.areaValue.getChannelPercentValue(yChannel, 1 - percent.y);
1120
+ const color = ctx2.areaValue.withChannelValue(xChannel, xValue).withChannelValue(yChannel, yValue);
1007
1121
  set.value(ctx2, color);
1008
1122
  },
1009
1123
  setChannelColorFromPoint(ctx2, evt) {
@@ -1013,13 +1127,16 @@ function machine(userContext) {
1013
1127
  return;
1014
1128
  const orientation = ctx2.activeOrientation || "horizontal";
1015
1129
  const channelPercent = orientation === "horizontal" ? percent.x : percent.y;
1016
- const value = ctx2.value.getChannelPercentValue(channel, channelPercent);
1017
- const color = ctx2.value.withChannelValue(channel, value);
1130
+ const value = ctx2.areaValue.getChannelPercentValue(channel, channelPercent);
1131
+ const color = ctx2.areaValue.withChannelValue(channel, value);
1018
1132
  set.value(ctx2, color);
1019
1133
  },
1020
1134
  setValue(ctx2, evt) {
1021
1135
  set.value(ctx2, evt.value);
1022
1136
  },
1137
+ setFormat(ctx2, evt) {
1138
+ set.format(ctx2, evt.format);
1139
+ },
1023
1140
  syncInputElements(ctx2) {
1024
1141
  sync.inputs(ctx2);
1025
1142
  },
@@ -1029,26 +1146,22 @@ function machine(userContext) {
1029
1146
  setChannelColorFromInput(ctx2, evt) {
1030
1147
  const { channel, isTextField, value } = evt;
1031
1148
  const currentAlpha = ctx2.value.getChannelValue("alpha");
1149
+ let color;
1032
1150
  if (channel === "alpha") {
1033
1151
  let valueAsNumber = parseFloat(value);
1034
1152
  valueAsNumber = Number.isNaN(valueAsNumber) ? currentAlpha : valueAsNumber;
1035
- const newColor = ctx2.value.withChannelValue("alpha", valueAsNumber);
1036
- set.value(ctx2, newColor);
1037
- return;
1038
- }
1039
- if (isTextField) {
1040
- const color2 = tryCatch(
1153
+ color = ctx2.value.withChannelValue("alpha", valueAsNumber);
1154
+ } else if (isTextField) {
1155
+ color = tryCatch(
1041
1156
  () => parse(value).withChannelValue("alpha", currentAlpha),
1042
1157
  () => ctx2.value
1043
1158
  );
1044
- const inputEls = dom.getChannelInputEl(ctx2, channel);
1045
- inputEls.forEach((inputEl) => {
1046
- dom.setValue(inputEl, getChannelValue(color2, channel));
1047
- });
1048
- set.value(ctx2, color2);
1049
- return;
1159
+ } else {
1160
+ const current = ctx2.value.toFormat(ctx2.format);
1161
+ const valueAsNumber = Number.isNaN(value) ? current.getChannelValue(channel) : value;
1162
+ color = current.withChannelValue(channel, valueAsNumber);
1050
1163
  }
1051
- const color = ctx2.value.toFormat(ctx2.value.outputFormat).withChannelValue(channel, value);
1164
+ sync.inputs(ctx2, color);
1052
1165
  set.value(ctx2, color);
1053
1166
  },
1054
1167
  incrementChannel(ctx2, evt) {
@@ -1061,22 +1174,22 @@ function machine(userContext) {
1061
1174
  },
1062
1175
  incrementXChannel(ctx2, evt) {
1063
1176
  const { xChannel } = evt.channel;
1064
- const color = ctx2.value.incrementChannel(xChannel, evt.step);
1177
+ const color = ctx2.areaValue.incrementChannel(xChannel, evt.step);
1065
1178
  set.value(ctx2, color);
1066
1179
  },
1067
1180
  decrementXChannel(ctx2, evt) {
1068
1181
  const { xChannel } = evt.channel;
1069
- const color = ctx2.value.decrementChannel(xChannel, evt.step);
1182
+ const color = ctx2.areaValue.decrementChannel(xChannel, evt.step);
1070
1183
  set.value(ctx2, color);
1071
1184
  },
1072
1185
  incrementYChannel(ctx2, evt) {
1073
1186
  const { yChannel } = evt.channel;
1074
- const color = ctx2.value.incrementChannel(yChannel, evt.step);
1187
+ const color = ctx2.areaValue.incrementChannel(yChannel, evt.step);
1075
1188
  set.value(ctx2, color);
1076
1189
  },
1077
1190
  decrementYChannel(ctx2, evt) {
1078
1191
  const { yChannel } = evt.channel;
1079
- const color = ctx2.value.decrementChannel(yChannel, evt.step);
1192
+ const color = ctx2.areaValue.decrementChannel(yChannel, evt.step);
1080
1193
  set.value(ctx2, color);
1081
1194
  },
1082
1195
  setChannelToMax(ctx2, evt) {
@@ -1109,6 +1222,9 @@ function machine(userContext) {
1109
1222
  dom?.focus(ctx2, dom.getTriggerEl(ctx2));
1110
1223
  });
1111
1224
  },
1225
+ syncFormatSelectElement(ctx2) {
1226
+ sync.formatSelect(ctx2);
1227
+ },
1112
1228
  invokeOnOpen(ctx2) {
1113
1229
  ctx2.onOpenChange?.({ open: true });
1114
1230
  },
@@ -1126,41 +1242,56 @@ function machine(userContext) {
1126
1242
  );
1127
1243
  }
1128
1244
  var sync = {
1129
- inputs(ctx) {
1245
+ // sync channel inputs
1246
+ inputs(ctx, color) {
1130
1247
  const channelInputs = dom.getChannelInputEls(ctx);
1131
- channelInputs.forEach((inputEl) => {
1132
- const channel = inputEl.dataset.channel;
1133
- dom.setValue(inputEl, getChannelValue(ctx.value, channel));
1248
+ raf(() => {
1249
+ channelInputs.forEach((inputEl) => {
1250
+ const channel = inputEl.dataset.channel;
1251
+ dom.setValue(inputEl, getChannelValue(color || ctx.value, channel));
1252
+ });
1253
+ });
1254
+ },
1255
+ // sync format select
1256
+ formatSelect(ctx) {
1257
+ const selectEl = dom.getFormatSelectEl(ctx);
1258
+ raf(() => {
1259
+ dom.setValue(selectEl, ctx.format);
1134
1260
  });
1135
1261
  }
1136
1262
  };
1137
1263
  var invoke = {
1138
1264
  changeEnd(ctx) {
1265
+ const value = ctx.value.toFormat(ctx.format);
1139
1266
  ctx.onValueChangeEnd?.({
1140
- value: ctx.value,
1267
+ value,
1141
1268
  valueAsString: ctx.valueAsString
1142
1269
  });
1143
1270
  },
1144
1271
  change(ctx) {
1272
+ const value = ctx.value.toFormat(ctx.format);
1145
1273
  ctx.onValueChange?.({
1146
- value: ctx.value,
1274
+ value,
1147
1275
  valueAsString: ctx.valueAsString
1148
1276
  });
1149
1277
  dispatchInputValueEvent(dom.getHiddenInputEl(ctx), { value: ctx.valueAsString });
1278
+ },
1279
+ formatChange(ctx) {
1280
+ ctx.onFormatChange?.({ format: ctx.format });
1150
1281
  }
1151
1282
  };
1152
1283
  var set = {
1153
1284
  value(ctx, color) {
1154
1285
  if (!color || ctx.value.isEqual(color))
1155
1286
  return;
1156
- const currentFormat = ctx.value.getFormat();
1157
- const outputColor = color.toFormat(currentFormat);
1158
- try {
1159
- outputColor.outputFormat = ctx.value.outputFormat;
1160
- } catch {
1161
- }
1162
- ctx.value = outputColor;
1287
+ ctx.value = color;
1163
1288
  invoke.change(ctx);
1289
+ },
1290
+ format(ctx, format) {
1291
+ if (ctx.format === format)
1292
+ return;
1293
+ ctx.format = format;
1294
+ invoke.formatChange(ctx);
1164
1295
  }
1165
1296
  };
1166
1297
  export {