@zag-js/color-picker 0.23.0 → 0.25.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.
@@ -1,101 +1,147 @@
1
1
  import { parseColor, type Color } from "@zag-js/color-utils"
2
- import { createMachine } from "@zag-js/core"
2
+ import { createMachine, guards, ref } from "@zag-js/core"
3
+ import { trackDismissableElement } from "@zag-js/dismissable"
3
4
  import { trackPointerMove } from "@zag-js/dom-event"
4
5
  import { raf } from "@zag-js/dom-query"
5
- import { trackFormControl } from "@zag-js/form-utils"
6
- import { clampValue, getPercentValue, snapValueToStep } from "@zag-js/numeric-range"
6
+ import { dispatchInputValueEvent, trackFormControl } from "@zag-js/form-utils"
7
+ import { getPlacement } from "@zag-js/popper"
7
8
  import { disableTextSelection } from "@zag-js/text-selection"
8
9
  import { compact, tryCatch } from "@zag-js/utils"
9
10
  import { dom } from "./color-picker.dom"
11
+ import { parse } from "./color-picker.parse"
10
12
  import type {
11
- ColorFormat,
12
13
  ColorType,
13
14
  ExtendedColorChannel,
14
15
  MachineContext,
15
16
  MachineState,
16
17
  UserDefinedContext,
17
18
  } from "./color-picker.types"
18
- import { getChannelDetails } from "./utils/get-channel-details"
19
- import { getChannelInputValue } from "./utils/get-channel-input-value"
19
+ import { getChannelValue } from "./utils/get-channel-input-value"
20
+
21
+ const { stateIn } = guards
20
22
 
21
23
  export function machine(userContext: UserDefinedContext) {
22
24
  const ctx = compact(userContext)
23
25
  return createMachine<MachineContext, MachineState>(
24
26
  {
25
27
  id: "color-picker",
26
- initial: "idle",
28
+ initial: ctx.open ? "open" : "idle",
27
29
  context: {
28
30
  dir: "ltr",
29
- value: "#D9D9D9",
31
+ value: parse("#000000"),
30
32
  disabled: false,
31
33
  ...ctx,
32
34
  activeId: null,
33
35
  activeChannel: null,
34
36
  activeOrientation: null,
35
37
  fieldsetDisabled: false,
38
+ autoFocus: true,
39
+ positioning: {
40
+ ...ctx.positioning,
41
+ placement: "bottom",
42
+ },
36
43
  },
37
44
 
38
45
  computed: {
39
46
  isRtl: (ctx) => ctx.dir === "rtl",
40
47
  isDisabled: (ctx) => !!ctx.disabled || ctx.fieldsetDisabled,
41
48
  isInteractive: (ctx) => !(ctx.isDisabled || ctx.readOnly),
42
- valueAsColor: (ctx) => parseColor(ctx.value) as Color,
43
- },
44
-
45
- on: {
46
- "VALUE.SET": {
47
- actions: ["setValue"],
49
+ valueAsString: (ctx) => {
50
+ const color = ctx.value.outputFormat ? ctx.value.toFormat(ctx.value.outputFormat) : ctx.value
51
+ return color.toString("css")
48
52
  },
49
53
  },
50
54
 
51
55
  activities: ["trackFormControl"],
52
56
 
53
57
  watch: {
54
- value: ["syncInputElements"],
58
+ valueAsString: ["syncInputElements"],
59
+ open: ["toggleVisibility"],
60
+ },
61
+
62
+ on: {
63
+ "VALUE.SET": {
64
+ actions: ["setValue"],
65
+ },
66
+ "CHANNEL_INPUT.FOCUS": [
67
+ {
68
+ guard: stateIn("idle"),
69
+ target: "focused",
70
+ actions: ["setActiveChannel"],
71
+ },
72
+ {
73
+ actions: ["setActiveChannel"],
74
+ },
75
+ ],
76
+ "CHANNEL_INPUT.BLUR": [
77
+ {
78
+ guard: stateIn("focused"),
79
+ target: "idle",
80
+ actions: ["setChannelColorFromInput"],
81
+ },
82
+ {
83
+ actions: ["setChannelColorFromInput"],
84
+ },
85
+ ],
86
+ "CHANNEL_INPUT.CHANGE": {
87
+ actions: ["setChannelColorFromInput"],
88
+ },
55
89
  },
56
90
 
57
91
  states: {
58
92
  idle: {
93
+ tags: ["closed"],
59
94
  on: {
95
+ OPEN: {
96
+ target: "open",
97
+ actions: ["setInitialFocus", "invokeOnOpen"],
98
+ },
99
+ "TRIGGER.CLICK": {
100
+ target: "open",
101
+ actions: ["setInitialFocus", "invokeOnOpen"],
102
+ },
103
+ },
104
+ },
105
+
106
+ focused: {
107
+ tags: ["closed", "focused"],
108
+ on: {
109
+ OPEN: {
110
+ target: "open",
111
+ actions: ["setInitialFocus", "invokeOnOpen"],
112
+ },
113
+ "TRIGGER.CLICK": {
114
+ target: "open",
115
+ actions: ["setInitialFocus", "invokeOnOpen"],
116
+ },
117
+ },
118
+ },
119
+
120
+ open: {
121
+ tags: ["open"],
122
+ activities: ["trackPositioning", "trackDismissableElement"],
123
+ on: {
124
+ "TRIGGER.CLICK": {
125
+ target: "idle",
126
+ actions: ["invokeOnClose"],
127
+ },
60
128
  "EYEDROPPER.CLICK": {
61
129
  actions: ["openEyeDropper"],
62
130
  },
63
131
  "AREA.POINTER_DOWN": {
64
- target: "dragging",
132
+ target: "open:dragging",
65
133
  actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"],
66
134
  },
67
135
  "AREA.FOCUS": {
68
- target: "focused",
69
136
  actions: ["setActiveChannel"],
70
137
  },
71
138
  "CHANNEL_SLIDER.POINTER_DOWN": {
72
- target: "dragging",
139
+ target: "open:dragging",
73
140
  actions: ["setActiveChannel", "setChannelColorFromPoint", "focusChannelThumb"],
74
141
  },
75
142
  "CHANNEL_SLIDER.FOCUS": {
76
- target: "focused",
77
- actions: ["setActiveChannel"],
78
- },
79
- "CHANNEL_INPUT.FOCUS": {
80
- target: "focused",
81
143
  actions: ["setActiveChannel"],
82
144
  },
83
- "CHANNEL_INPUT.CHANGE": {
84
- actions: ["setChannelColorFromInput"],
85
- },
86
- },
87
- },
88
-
89
- focused: {
90
- on: {
91
- "AREA.POINTER_DOWN": {
92
- target: "dragging",
93
- actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"],
94
- },
95
- "CHANNEL_SLIDER.POINTER_DOWN": {
96
- target: "dragging",
97
- actions: ["setActiveChannel", "setChannelColorFromPoint", "focusChannelThumb"],
98
- },
99
145
  "AREA.ARROW_LEFT": {
100
146
  actions: ["decrementXChannel"],
101
147
  },
@@ -138,45 +184,100 @@ export function machine(userContext: UserDefinedContext) {
138
184
  "CHANNEL_SLIDER.END": {
139
185
  actions: ["setChannelToMax"],
140
186
  },
141
- "CHANNEL_INPUT.FOCUS": {
142
- actions: ["setActiveChannel"],
143
- },
144
- "CHANNEL_INPUT.CHANGE": {
145
- actions: ["setChannelColorFromInput"],
146
- },
147
- "CHANNEL_SLIDER.BLUR": {
148
- target: "idle",
149
- },
150
- "AREA.BLUR": {
187
+ INTERACT_OUTSIDE: [
188
+ {
189
+ guard: "shouldRestoreFocus",
190
+ target: "focused",
191
+ actions: ["setReturnFocus", "invokeOnClose"],
192
+ },
193
+ {
194
+ target: "idle",
195
+ actions: ["invokeOnClose"],
196
+ },
197
+ ],
198
+ CLOSE: {
151
199
  target: "idle",
200
+ actions: ["invokeOnClose"],
152
201
  },
153
202
  },
154
203
  },
155
204
 
156
- dragging: {
205
+ "open:dragging": {
206
+ tags: ["open"],
157
207
  exit: ["clearActiveChannel"],
158
- activities: ["trackPointerMove", "disableTextSelection"],
208
+ activities: ["trackPointerMove", "disableTextSelection", "trackPositioning", "trackDismissableElement"],
159
209
  on: {
160
210
  "AREA.POINTER_MOVE": {
161
- actions: ["setAreaColorFromPoint"],
211
+ actions: ["setAreaColorFromPoint", "focusAreaThumb"],
162
212
  },
163
213
  "AREA.POINTER_UP": {
164
- target: "focused",
214
+ target: "open",
165
215
  actions: ["invokeOnChangeEnd"],
166
216
  },
167
217
  "CHANNEL_SLIDER.POINTER_MOVE": {
168
- actions: ["setChannelColorFromPoint"],
218
+ actions: ["setChannelColorFromPoint", "focusChannelThumb"],
169
219
  },
170
220
  "CHANNEL_SLIDER.POINTER_UP": {
171
- target: "focused",
221
+ target: "open",
172
222
  actions: ["invokeOnChangeEnd"],
173
223
  },
224
+ INTERACT_OUTSIDE: [
225
+ {
226
+ guard: "shouldRestoreFocus",
227
+ target: "focused",
228
+ actions: ["setReturnFocus", "invokeOnClose"],
229
+ },
230
+ {
231
+ target: "idle",
232
+ actions: ["invokeOnClose"],
233
+ },
234
+ ],
235
+ CLOSE: {
236
+ target: "idle",
237
+ actions: ["invokeOnClose"],
238
+ },
174
239
  },
175
240
  },
176
241
  },
177
242
  },
178
243
  {
244
+ guards: {
245
+ isTargetFocusable: (_ctx, evt) => evt.restoreFocus,
246
+ },
179
247
  activities: {
248
+ trackPositioning(ctx) {
249
+ ctx.currentPlacement = ctx.positioning.placement
250
+ const anchorEl = dom.getTriggerEl(ctx)
251
+ const getPositionerEl = () => dom.getPositionerEl(ctx)
252
+ return getPlacement(anchorEl, getPositionerEl, {
253
+ ...ctx.positioning,
254
+ defer: true,
255
+ onComplete(data) {
256
+ ctx.currentPlacement = data.placement
257
+ },
258
+ onCleanup() {
259
+ ctx.currentPlacement = undefined
260
+ },
261
+ })
262
+ },
263
+ trackDismissableElement(ctx, _evt, { send }) {
264
+ const getContentEl = () => dom.getContentEl(ctx)
265
+ let restoreFocus = true
266
+ return trackDismissableElement(getContentEl, {
267
+ exclude: dom.getTriggerEl(ctx),
268
+ defer: true,
269
+ onInteractOutside(event) {
270
+ ctx.onInteractOutside?.(event)
271
+ if (event.defaultPrevented) return
272
+ restoreFocus = !(event.detail.focusable || event.detail.contextmenu)
273
+ },
274
+ onPointerDownOutside: ctx.onPointerDownOutside,
275
+ onFocusOutside: ctx.onFocusOutside,
276
+ onDismiss() {
277
+ send({ type: "INTERACT_OUTSIDE", restoreFocus })
278
+ },
279
+ })
280
+ },
180
281
  trackFormControl(ctx, _evt, { send, initialContext }) {
181
282
  const inputEl = dom.getHiddenInputEl(ctx)
182
283
  return trackFormControl(inputEl, {
@@ -213,21 +314,17 @@ export function machine(userContext: UserDefinedContext) {
213
314
  picker
214
315
  .open()
215
316
  .then(({ sRGBHex }: { sRGBHex: string }) => {
216
- const format = ctx.valueAsColor.getColorFormat()
317
+ const format = ctx.value.getFormat()
217
318
  const color = parseColor(sRGBHex).toFormat(format) as Color
218
319
  set.value(ctx, color)
219
- ctx.onValueChangeEnd?.({ value: ctx.value, valueAsColor: color })
320
+ ctx.onValueChangeEnd?.({ value: ctx.value, valueAsString: ctx.valueAsString })
220
321
  })
221
322
  .catch(() => void 0)
222
323
  },
223
324
  setActiveChannel(ctx, evt) {
224
325
  ctx.activeId = evt.id
225
- if (evt.channel) {
226
- ctx.activeChannel = evt.channel
227
- }
228
- if (evt.orientation) {
229
- ctx.activeOrientation = evt.orientation
230
- }
326
+ if (evt.channel) ctx.activeChannel = evt.channel
327
+ if (evt.orientation) ctx.activeOrientation = evt.orientation
231
328
  },
232
329
  clearActiveChannel(ctx) {
233
330
  ctx.activeChannel = null
@@ -240,10 +337,10 @@ export function machine(userContext: UserDefinedContext) {
240
337
  const percent = dom.getAreaValueFromPoint(ctx, evt.point)
241
338
  if (!percent) return
242
339
 
243
- const { getColorFromPoint } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
244
- const color = getColorFromPoint(percent.x, percent.y)
340
+ const xValue = ctx.value.getChannelPercentValue(xChannel, percent.x)
341
+ const yValue = ctx.value.getChannelPercentValue(yChannel, 1 - percent.y)
245
342
 
246
- if (!color) return
343
+ const color = ctx.value.withChannelValue(xChannel, xValue).withChannelValue(yChannel, yValue)
247
344
  set.value(ctx, color)
248
345
  },
249
346
  setChannelColorFromPoint(ctx, evt) {
@@ -252,30 +349,18 @@ export function machine(userContext: UserDefinedContext) {
252
349
  const percent = dom.getChannelSliderValueFromPoint(ctx, evt.point, channel)
253
350
  if (!percent) return
254
351
 
255
- const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(channel)
256
352
  const orientation = ctx.activeOrientation || "horizontal"
353
+ const channelPercent = orientation === "horizontal" ? percent.x : percent.y
257
354
 
258
- const point = orientation === "horizontal" ? percent.x : percent.y
259
- const channelValue = getPercentValue(point, minValue, maxValue, step)
260
-
261
- const value = snapValueToStep(channelValue - step, minValue, maxValue, step)
262
- const newColor = ctx.valueAsColor.withChannelValue(channel, value)
263
-
264
- set.value(ctx, newColor)
355
+ const value = ctx.value.getChannelPercentValue(channel, channelPercent)
356
+ const color = ctx.value.withChannelValue(channel, value)
357
+ set.value(ctx, color)
265
358
  },
266
359
  setValue(ctx, evt) {
267
360
  set.value(ctx, evt.value)
268
361
  },
269
362
  syncInputElements(ctx) {
270
- // sync channel inputs
271
- const inputs = dom.getChannelInputEls(ctx)
272
- inputs.forEach((input) => {
273
- const channel = input.dataset.channel as ExtendedColorChannel | null
274
- dom.setValue(input, getChannelInputValue(ctx.valueAsColor, channel))
275
- })
276
-
277
- // sync hidden input
278
- dom.setValue(dom.getHiddenInputEl(ctx), ctx.value)
363
+ sync.inputs(ctx)
279
364
  },
280
365
  invokeOnChangeEnd(ctx) {
281
366
  invoke.changeEnd(ctx)
@@ -285,119 +370,146 @@ export function machine(userContext: UserDefinedContext) {
285
370
 
286
371
  // handle alpha channel
287
372
  if (channel === "alpha") {
288
- const newColor = ctx.valueAsColor.withChannelValue("alpha", value)
373
+ const newColor = ctx.value.withChannelValue("alpha", parseFloat(value))
289
374
  set.value(ctx, newColor)
290
375
  return
291
376
  }
292
377
 
293
378
  // handle other text channels
294
379
  if (isTextField) {
295
- const format: ColorFormat = "hsl"
296
- const currentAlpha = ctx.valueAsColor.getChannelValue("alpha")
380
+ const currentAlpha = ctx.value.getChannelValue("alpha")
297
381
 
298
- const newColor = tryCatch(
299
- () => parseColor(value).toFormat(format).withChannelValue("alpha", currentAlpha),
300
- () => ctx.valueAsColor,
382
+ const color = tryCatch(
383
+ () => parse(value).withChannelValue("alpha", currentAlpha),
384
+ () => ctx.value,
301
385
  )
302
386
 
303
387
  // set channel input value immediately (in event user types native css color, we need to convert it to the current channel format)
304
388
  const inputEl = dom.getChannelInputEl(ctx, channel)
305
- dom.setValue(inputEl, getChannelInputValue(ctx.valueAsColor, channel))
389
+ dom.setValue(inputEl, getChannelValue(color, channel))
306
390
 
307
- set.value(ctx, newColor)
391
+ set.value(ctx, color)
308
392
  return
309
393
  }
310
394
 
311
395
  // handle other channels
312
- const newColor = ctx.valueAsColor.withChannelValue(channel, value)
313
- set.value(ctx, newColor)
396
+ const color = ctx.value.toFormat(ctx.value.outputFormat!).withChannelValue(channel, value)
397
+ set.value(ctx, color)
314
398
  },
315
-
316
399
  incrementChannel(ctx, evt) {
317
- const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(evt.channel)
318
- const channelValue = ctx.valueAsColor.getChannelValue(evt.channel)
319
- const value = snapValueToStep(channelValue + evt.step, minValue, maxValue, step)
320
- const color = ctx.valueAsColor.withChannelValue(evt.channel, clampValue(value, minValue, maxValue))
400
+ const color = ctx.value.incrementChannel(evt.channel, evt.step)
321
401
  set.value(ctx, color)
322
402
  },
323
403
  decrementChannel(ctx, evt) {
324
- const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(evt.channel)
325
- const channelValue = ctx.valueAsColor.getChannelValue(evt.channel)
326
- const value = snapValueToStep(channelValue - evt.step, minValue, maxValue, step)
327
- const color = ctx.valueAsColor.withChannelValue(evt.channel, clampValue(value, minValue, maxValue))
404
+ const color = ctx.value.decrementChannel(evt.channel, evt.step)
328
405
  set.value(ctx, color)
329
406
  },
330
-
331
407
  incrementXChannel(ctx, evt) {
332
- const { xChannel, yChannel } = evt.channel
333
- const { incrementX } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
334
- const color = ctx.valueAsColor.withChannelValue(xChannel, incrementX(evt.step))
408
+ const { xChannel } = evt.channel
409
+ const color = ctx.value.incrementChannel(xChannel, evt.step)
335
410
  set.value(ctx, color)
336
411
  },
337
412
  decrementXChannel(ctx, evt) {
338
- const { xChannel, yChannel } = evt.channel
339
- const { decrementX } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
340
- const color = ctx.valueAsColor.withChannelValue(xChannel, decrementX(evt.step))
413
+ const { xChannel } = evt.channel
414
+ const color = ctx.value.decrementChannel(xChannel, evt.step)
341
415
  set.value(ctx, color)
342
416
  },
343
-
344
417
  incrementYChannel(ctx, evt) {
345
- const { xChannel, yChannel } = evt.channel
346
- const { incrementY } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
347
- const color = ctx.valueAsColor.withChannelValue(yChannel, incrementY(evt.step))
418
+ const { yChannel } = evt.channel
419
+ const color = ctx.value.incrementChannel(yChannel, evt.step)
348
420
  set.value(ctx, color)
349
421
  },
350
422
  decrementYChannel(ctx, evt) {
351
- const { xChannel, yChannel } = evt.channel
352
- const { decrementY } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
353
- const color = ctx.valueAsColor.withChannelValue(yChannel, decrementY(evt.step))
423
+ const { yChannel } = evt.channel
424
+ const color = ctx.value.decrementChannel(yChannel, evt.step)
354
425
  set.value(ctx, color)
355
426
  },
356
-
357
427
  setChannelToMax(ctx, evt) {
358
- const { maxValue } = ctx.valueAsColor.getChannelRange(evt.channel)
359
- const color = ctx.valueAsColor.withChannelValue(evt.channel, maxValue)
428
+ const range = ctx.value.getChannelRange(evt.channel)
429
+ const color = ctx.value.withChannelValue(evt.channel, range.maxValue)
360
430
  set.value(ctx, color)
361
431
  },
362
432
  setChannelToMin(ctx, evt) {
363
- const { minValue } = ctx.valueAsColor.getChannelRange(evt.channel)
364
- const color = ctx.valueAsColor.withChannelValue(evt.channel, minValue)
433
+ const range = ctx.value.getChannelRange(evt.channel)
434
+ const color = ctx.value.withChannelValue(evt.channel, range.minValue)
365
435
  set.value(ctx, color)
366
436
  },
367
437
  focusAreaThumb(ctx) {
368
438
  raf(() => {
369
- dom.getAreaThumbEl(ctx)?.focus({ preventScroll: true })
439
+ dom?.focus(ctx, dom.getAreaThumbEl(ctx))
370
440
  })
371
441
  },
372
442
  focusChannelThumb(ctx, evt) {
373
443
  raf(() => {
374
- dom.getChannelSliderThumbEl(ctx, evt.channel)?.focus({ preventScroll: true })
444
+ dom?.focus(ctx, dom.getChannelSliderThumbEl(ctx, evt.channel))
445
+ })
446
+ },
447
+ setInitialFocus(ctx) {
448
+ raf(() => {
449
+ dom.getInitialFocusEl(ctx)?.focus({ preventScroll: true })
450
+ })
451
+ },
452
+ setReturnFocus(ctx) {
453
+ raf(() => {
454
+ dom?.focus(ctx, dom.getTriggerEl(ctx))
375
455
  })
376
456
  },
457
+ invokeOnOpen(ctx) {
458
+ ctx.onOpenChange?.({ open: true })
459
+ },
460
+ invokeOnClose(ctx) {
461
+ ctx.onOpenChange?.({ open: false })
462
+ },
463
+ toggleVisibility(ctx, _evt, { send }) {
464
+ send({ type: ctx.open ? "OPEN" : "CLOSE", src: "controlled" })
465
+ },
466
+ },
467
+ compareFns: {
468
+ value: (a, b) => a.isEqual(b),
377
469
  },
378
470
  },
379
471
  )
380
472
  }
381
473
 
474
+ const sync = {
475
+ inputs(ctx: MachineContext) {
476
+ // sync channel inputs
477
+ const channelInputs = dom.getChannelInputEls(ctx)
478
+ channelInputs.forEach((inputEl) => {
479
+ const channel = inputEl.dataset.channel as ExtendedColorChannel | null
480
+ dom.setValue(inputEl, getChannelValue(ctx.value, channel))
481
+ })
482
+ },
483
+ }
484
+
382
485
  const invoke = {
383
486
  changeEnd(ctx: MachineContext) {
384
487
  ctx.onValueChangeEnd?.({
385
488
  value: ctx.value,
386
- valueAsColor: ctx.valueAsColor,
489
+ valueAsString: ctx.valueAsString,
387
490
  })
388
491
  },
389
492
  change(ctx: MachineContext) {
390
493
  ctx.onValueChange?.({
391
494
  value: ctx.value,
392
- valueAsColor: ctx.valueAsColor,
495
+ valueAsString: ctx.valueAsString,
393
496
  })
497
+
498
+ dispatchInputValueEvent(dom.getHiddenInputEl(ctx), { value: ctx.valueAsString })
394
499
  },
395
500
  }
396
501
 
397
502
  const set = {
398
- value(ctx: MachineContext, color: Color | ColorType) {
399
- if (ctx.valueAsColor.isEqual(color)) return
400
- ctx.value = color.toString("css")
503
+ value(ctx: MachineContext, color: Color | ColorType | undefined) {
504
+ sync.inputs(ctx)
505
+ if (!color || ctx.value.isEqual(color)) return
506
+ const currentFormat = ctx.value.getFormat()
507
+ const outputColor = color.toFormat(currentFormat)
508
+ try {
509
+ outputColor.outputFormat = ctx.value.outputFormat
510
+ } catch {}
511
+
512
+ ctx.value = ref(outputColor) as Color
401
513
  invoke.change(ctx)
402
514
  },
403
515
  }
@@ -0,0 +1,14 @@
1
+ import { parseColor, type Color } from "@zag-js/color-utils"
2
+ import { ref } from "@zag-js/core"
3
+
4
+ export const parse = (color: string): Color => {
5
+ const value = parseColor(color)
6
+
7
+ const colorFormat = value.getFormat()
8
+ const lockedFormat = colorFormat.startsWith("hsl") ? "hsla" : "hsba"
9
+
10
+ const result = value.toFormat(lockedFormat)
11
+ result.outputFormat = colorFormat
12
+
13
+ return ref(result)
14
+ }