@zag-js/color-picker 0.29.0 → 0.31.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,86 @@ 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
+ hidden: !triggerState.isChecked
723
+ });
724
+ },
660
725
  getSwatchProps(props) {
661
- const { value: valueProp, respectAlpha = true } = props;
662
- const colorValue = normalizeColor(valueProp);
663
- const color = colorValue.toFormat(value.getFormat());
726
+ const { respectAlpha = true } = props;
727
+ const triggerState = getSwatchTriggerState(props);
664
728
  return normalize.element({
665
729
  ...parts.swatch.attrs,
666
730
  dir: state.context.dir,
667
- "data-state": colorValue.isEqual(value) ? "selected" : "unselected",
668
- "data-value": color.toString("hex"),
731
+ "data-state": triggerState.isChecked ? "checked" : "unchecked",
732
+ "data-value": triggerState.valueAsString,
669
733
  style: {
670
734
  position: "relative",
671
- background: color.toString(respectAlpha ? "css" : "hex")
735
+ background: triggerState.value.toString(respectAlpha ? "css" : "hex")
672
736
  }
673
737
  });
674
- }
738
+ },
739
+ formatTriggerProps: normalize.button({
740
+ ...parts.formatTrigger.attrs,
741
+ dir: state.context.dir,
742
+ type: "button",
743
+ "aria-label": `change color format to ${getNextFormat(state.context.format)}`,
744
+ onClick(event) {
745
+ if (event.currentTarget.disabled)
746
+ return;
747
+ const nextFormat = getNextFormat(state.context.format);
748
+ send({ type: "FORMAT.SET", format: nextFormat, src: "format-trigger" });
749
+ }
750
+ }),
751
+ formatSelectProps: normalize.select({
752
+ ...parts.formatSelect.attrs,
753
+ "aria-label": "change color format",
754
+ dir: state.context.dir,
755
+ defaultValue: state.context.format,
756
+ disabled: isDisabled,
757
+ onChange(event) {
758
+ const format = assertFormat(event.currentTarget.value);
759
+ send({ type: "FORMAT.SET", format, src: "format-select" });
760
+ }
761
+ })
675
762
  };
676
763
  }
764
+ var formats = ["hsba", "hsla", "rgba"];
765
+ var formatRegex = new RegExp(`^(${formats.join("|")})$`);
766
+ function getNextFormat(format) {
767
+ const index = formats.indexOf(format);
768
+ return formats[index + 1] ?? formats[0];
769
+ }
770
+ function assertFormat(format) {
771
+ if (formatRegex.test(format))
772
+ return format;
773
+ throw new Error(`Unsupported color format: ${format}`);
774
+ }
677
775
 
678
776
  // src/color-picker.machine.ts
679
777
  import { parseColor as parseColor4 } from "@zag-js/color-utils";
@@ -688,13 +786,8 @@ import { compact, tryCatch } from "@zag-js/utils";
688
786
 
689
787
  // src/color-picker.parse.ts
690
788
  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;
789
+ var parse = (colorString) => {
790
+ return parseColor3(colorString);
698
791
  };
699
792
 
700
793
  // src/color-picker.machine.ts
@@ -707,13 +800,14 @@ function machine(userContext) {
707
800
  context: {
708
801
  dir: "ltr",
709
802
  value: parse("#000000"),
803
+ format: "rgba",
710
804
  disabled: false,
805
+ closeOnSelect: false,
711
806
  ...ctx,
712
807
  activeId: null,
713
808
  activeChannel: null,
714
809
  activeOrientation: null,
715
810
  fieldsetDisabled: false,
716
- autoFocus: true,
717
811
  positioning: {
718
812
  ...ctx.positioning,
719
813
  placement: "bottom"
@@ -723,22 +817,30 @@ function machine(userContext) {
723
817
  isRtl: (ctx2) => ctx2.dir === "rtl",
724
818
  isDisabled: (ctx2) => !!ctx2.disabled || ctx2.fieldsetDisabled,
725
819
  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");
820
+ valueAsString: (ctx2) => ctx2.value.toString(ctx2.format),
821
+ areaValue: (ctx2) => {
822
+ const format = ctx2.format.startsWith("hsl") ? "hsla" : "hsba";
823
+ return ctx2.value.toFormat(format);
729
824
  }
730
825
  },
731
826
  activities: ["trackFormControl"],
732
827
  watch: {
733
828
  value: ["syncInputElements"],
829
+ format: ["syncFormatSelectElement"],
734
830
  open: ["toggleVisibility"]
735
831
  },
736
832
  on: {
737
833
  "VALUE.SET": {
738
834
  actions: ["setValue"]
739
835
  },
836
+ "FORMAT.SET": {
837
+ actions: ["setFormat"]
838
+ },
740
839
  "CHANNEL_INPUT.CHANGE": {
741
840
  actions: ["setChannelColorFromInput"]
841
+ },
842
+ "EYEDROPPER.CLICK": {
843
+ actions: ["openEyeDropper"]
742
844
  }
743
845
  },
744
846
  states: {
@@ -776,6 +878,9 @@ function machine(userContext) {
776
878
  "CHANNEL_INPUT.BLUR": {
777
879
  target: "idle",
778
880
  actions: ["setChannelColorFromInput"]
881
+ },
882
+ "TRIGGER.BLUR": {
883
+ target: "idle"
779
884
  }
780
885
  }
781
886
  },
@@ -787,9 +892,6 @@ function machine(userContext) {
787
892
  target: "idle",
788
893
  actions: ["invokeOnClose"]
789
894
  },
790
- "EYEDROPPER.CLICK": {
791
- actions: ["openEyeDropper"]
792
- },
793
895
  "AREA.POINTER_DOWN": {
794
896
  target: "open:dragging",
795
897
  actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"]
@@ -863,7 +965,17 @@ function machine(userContext) {
863
965
  CLOSE: {
864
966
  target: "idle",
865
967
  actions: ["invokeOnClose"]
866
- }
968
+ },
969
+ "SWATCH_TRIGGER.CLICK": [
970
+ {
971
+ guard: "closeOnSelect",
972
+ target: "focused",
973
+ actions: ["setValue", "setReturnFocus", "invokeOnClose"]
974
+ },
975
+ {
976
+ actions: ["setValue"]
977
+ }
978
+ ]
867
979
  }
868
980
  },
869
981
  "open:dragging": {
@@ -906,7 +1018,8 @@ function machine(userContext) {
906
1018
  },
907
1019
  {
908
1020
  guards: {
909
- isTargetFocusable: (_ctx, evt) => evt.restoreFocus
1021
+ isTargetFocusable: (_ctx, evt) => evt.restoreFocus,
1022
+ closeOnSelect: (ctx2) => !!ctx2.closeOnSelect
910
1023
  },
911
1024
  activities: {
912
1025
  trackPositioning(ctx2) {
@@ -1001,9 +1114,9 @@ function machine(userContext) {
1001
1114
  const percent = dom.getAreaValueFromPoint(ctx2, evt.point);
1002
1115
  if (!percent)
1003
1116
  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);
1117
+ const xValue = ctx2.areaValue.getChannelPercentValue(xChannel, percent.x);
1118
+ const yValue = ctx2.areaValue.getChannelPercentValue(yChannel, 1 - percent.y);
1119
+ const color = ctx2.areaValue.withChannelValue(xChannel, xValue).withChannelValue(yChannel, yValue);
1007
1120
  set.value(ctx2, color);
1008
1121
  },
1009
1122
  setChannelColorFromPoint(ctx2, evt) {
@@ -1013,13 +1126,16 @@ function machine(userContext) {
1013
1126
  return;
1014
1127
  const orientation = ctx2.activeOrientation || "horizontal";
1015
1128
  const channelPercent = orientation === "horizontal" ? percent.x : percent.y;
1016
- const value = ctx2.value.getChannelPercentValue(channel, channelPercent);
1017
- const color = ctx2.value.withChannelValue(channel, value);
1129
+ const value = ctx2.areaValue.getChannelPercentValue(channel, channelPercent);
1130
+ const color = ctx2.areaValue.withChannelValue(channel, value);
1018
1131
  set.value(ctx2, color);
1019
1132
  },
1020
1133
  setValue(ctx2, evt) {
1021
1134
  set.value(ctx2, evt.value);
1022
1135
  },
1136
+ setFormat(ctx2, evt) {
1137
+ set.format(ctx2, evt.format);
1138
+ },
1023
1139
  syncInputElements(ctx2) {
1024
1140
  sync.inputs(ctx2);
1025
1141
  },
@@ -1029,26 +1145,22 @@ function machine(userContext) {
1029
1145
  setChannelColorFromInput(ctx2, evt) {
1030
1146
  const { channel, isTextField, value } = evt;
1031
1147
  const currentAlpha = ctx2.value.getChannelValue("alpha");
1148
+ let color;
1032
1149
  if (channel === "alpha") {
1033
1150
  let valueAsNumber = parseFloat(value);
1034
1151
  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(
1152
+ color = ctx2.value.withChannelValue("alpha", valueAsNumber);
1153
+ } else if (isTextField) {
1154
+ color = tryCatch(
1041
1155
  () => parse(value).withChannelValue("alpha", currentAlpha),
1042
1156
  () => ctx2.value
1043
1157
  );
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;
1158
+ } else {
1159
+ const current = ctx2.value.toFormat(ctx2.format);
1160
+ const valueAsNumber = Number.isNaN(value) ? current.getChannelValue(channel) : value;
1161
+ color = current.withChannelValue(channel, valueAsNumber);
1050
1162
  }
1051
- const color = ctx2.value.toFormat(ctx2.value.outputFormat).withChannelValue(channel, value);
1163
+ sync.inputs(ctx2, color);
1052
1164
  set.value(ctx2, color);
1053
1165
  },
1054
1166
  incrementChannel(ctx2, evt) {
@@ -1061,22 +1173,22 @@ function machine(userContext) {
1061
1173
  },
1062
1174
  incrementXChannel(ctx2, evt) {
1063
1175
  const { xChannel } = evt.channel;
1064
- const color = ctx2.value.incrementChannel(xChannel, evt.step);
1176
+ const color = ctx2.areaValue.incrementChannel(xChannel, evt.step);
1065
1177
  set.value(ctx2, color);
1066
1178
  },
1067
1179
  decrementXChannel(ctx2, evt) {
1068
1180
  const { xChannel } = evt.channel;
1069
- const color = ctx2.value.decrementChannel(xChannel, evt.step);
1181
+ const color = ctx2.areaValue.decrementChannel(xChannel, evt.step);
1070
1182
  set.value(ctx2, color);
1071
1183
  },
1072
1184
  incrementYChannel(ctx2, evt) {
1073
1185
  const { yChannel } = evt.channel;
1074
- const color = ctx2.value.incrementChannel(yChannel, evt.step);
1186
+ const color = ctx2.areaValue.incrementChannel(yChannel, evt.step);
1075
1187
  set.value(ctx2, color);
1076
1188
  },
1077
1189
  decrementYChannel(ctx2, evt) {
1078
1190
  const { yChannel } = evt.channel;
1079
- const color = ctx2.value.decrementChannel(yChannel, evt.step);
1191
+ const color = ctx2.areaValue.decrementChannel(yChannel, evt.step);
1080
1192
  set.value(ctx2, color);
1081
1193
  },
1082
1194
  setChannelToMax(ctx2, evt) {
@@ -1109,6 +1221,9 @@ function machine(userContext) {
1109
1221
  dom?.focus(ctx2, dom.getTriggerEl(ctx2));
1110
1222
  });
1111
1223
  },
1224
+ syncFormatSelectElement(ctx2) {
1225
+ sync.formatSelect(ctx2);
1226
+ },
1112
1227
  invokeOnOpen(ctx2) {
1113
1228
  ctx2.onOpenChange?.({ open: true });
1114
1229
  },
@@ -1126,41 +1241,56 @@ function machine(userContext) {
1126
1241
  );
1127
1242
  }
1128
1243
  var sync = {
1129
- inputs(ctx) {
1244
+ // sync channel inputs
1245
+ inputs(ctx, color) {
1130
1246
  const channelInputs = dom.getChannelInputEls(ctx);
1131
- channelInputs.forEach((inputEl) => {
1132
- const channel = inputEl.dataset.channel;
1133
- dom.setValue(inputEl, getChannelValue(ctx.value, channel));
1247
+ raf(() => {
1248
+ channelInputs.forEach((inputEl) => {
1249
+ const channel = inputEl.dataset.channel;
1250
+ dom.setValue(inputEl, getChannelValue(color || ctx.value, channel));
1251
+ });
1252
+ });
1253
+ },
1254
+ // sync format select
1255
+ formatSelect(ctx) {
1256
+ const selectEl = dom.getFormatSelectEl(ctx);
1257
+ raf(() => {
1258
+ dom.setValue(selectEl, ctx.format);
1134
1259
  });
1135
1260
  }
1136
1261
  };
1137
1262
  var invoke = {
1138
1263
  changeEnd(ctx) {
1264
+ const value = ctx.value.toFormat(ctx.format);
1139
1265
  ctx.onValueChangeEnd?.({
1140
- value: ctx.value,
1266
+ value,
1141
1267
  valueAsString: ctx.valueAsString
1142
1268
  });
1143
1269
  },
1144
1270
  change(ctx) {
1271
+ const value = ctx.value.toFormat(ctx.format);
1145
1272
  ctx.onValueChange?.({
1146
- value: ctx.value,
1273
+ value,
1147
1274
  valueAsString: ctx.valueAsString
1148
1275
  });
1149
1276
  dispatchInputValueEvent(dom.getHiddenInputEl(ctx), { value: ctx.valueAsString });
1277
+ },
1278
+ formatChange(ctx) {
1279
+ ctx.onFormatChange?.({ format: ctx.format });
1150
1280
  }
1151
1281
  };
1152
1282
  var set = {
1153
1283
  value(ctx, color) {
1154
1284
  if (!color || ctx.value.isEqual(color))
1155
1285
  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;
1286
+ ctx.value = color;
1163
1287
  invoke.change(ctx);
1288
+ },
1289
+ format(ctx, format) {
1290
+ if (ctx.format === format)
1291
+ return;
1292
+ ctx.format = format;
1293
+ invoke.formatChange(ctx);
1164
1294
  }
1165
1295
  };
1166
1296
  export {