@zag-js/color-picker 0.9.2 → 0.10.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.
@@ -0,0 +1,358 @@
1
+ import { Color, parseColor } from "@zag-js/color-utils"
2
+ import { createMachine } from "@zag-js/core"
3
+ import { trackPointerMove } from "@zag-js/dom-event"
4
+ import { raf } from "@zag-js/dom-query"
5
+ import { clampValue, getPercentValue, snapValueToStep } from "@zag-js/numeric-range"
6
+ import { disableTextSelection } from "@zag-js/text-selection"
7
+ import { compact } from "@zag-js/utils"
8
+ import { dom } from "./color-picker.dom"
9
+ import { ExtendedColorChannel, MachineContext, MachineState, UserDefinedContext } from "./color-picker.types"
10
+ import { getChannelDetails } from "./utils/get-channel-details"
11
+ import { getChannelInputValue } from "./utils/get-channel-input-value"
12
+
13
+ export function machine(userContext: UserDefinedContext) {
14
+ const ctx = compact(userContext)
15
+
16
+ return createMachine<MachineContext, MachineState>(
17
+ {
18
+ id: "color-picker",
19
+ initial: "idle",
20
+ context: {
21
+ dir: "ltr",
22
+ activeId: null,
23
+ activeChannel: null,
24
+ activeOrientation: null,
25
+ value: "#D9D9D9",
26
+ ...ctx,
27
+ valueAsColor: parseColor(ctx.value || "#D9D9D9"),
28
+ },
29
+
30
+ computed: {
31
+ isRtl: (ctx) => ctx.dir === "rtl",
32
+ isInteractive: (ctx) => !(ctx.disabled || ctx.readOnly),
33
+ },
34
+
35
+ on: {
36
+ "VALUE.SET": {
37
+ actions: ["setValue"],
38
+ },
39
+ },
40
+
41
+ created: ["setValueAsColor"],
42
+
43
+ watch: {
44
+ value: ["setValueAsColor", "syncChannelInputs", "invokeOnChange"],
45
+ },
46
+
47
+ states: {
48
+ idle: {
49
+ on: {
50
+ "EYEDROPPER.CLICK": {
51
+ actions: ["openEyeDropper"],
52
+ },
53
+ "AREA.POINTER_DOWN": {
54
+ target: "dragging",
55
+ actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"],
56
+ },
57
+ "CHANNEL_SLIDER.POINTER_DOWN": {
58
+ target: "dragging",
59
+ actions: ["setActiveChannel", "setChannelColorFromPoint", "focusChannelThumb"],
60
+ },
61
+ "CHANNEL_INPUT.FOCUS": {
62
+ target: "focused",
63
+ actions: ["setActiveChannel"],
64
+ },
65
+ "CHANNEL_INPUT.CHANGE": {
66
+ actions: ["setChannelColorFromInput"],
67
+ },
68
+ },
69
+ },
70
+
71
+ focused: {
72
+ on: {
73
+ "AREA.POINTER_DOWN": {
74
+ target: "dragging",
75
+ actions: ["setActiveChannel", "setAreaColorFromPoint", "focusAreaThumb"],
76
+ },
77
+ "CHANNEL_SLIDER.POINTER_DOWN": {
78
+ target: "dragging",
79
+ actions: ["setActiveChannel", "setChannelColorFromPoint", "focusChannelThumb"],
80
+ },
81
+ "AREA.ARROW_LEFT": {
82
+ actions: ["decrementXChannel"],
83
+ },
84
+ "AREA.ARROW_RIGHT": {
85
+ actions: ["incrementXChannel"],
86
+ },
87
+ "AREA.ARROW_UP": {
88
+ actions: ["incrementYChannel"],
89
+ },
90
+ "AREA.ARROW_DOWN": {
91
+ actions: ["decrementYChannel"],
92
+ },
93
+ "AREA.PAGE_UP": {
94
+ actions: ["incrementXChannel"],
95
+ },
96
+ "AREA.PAGE_DOWN": {
97
+ actions: ["decrementXChannel"],
98
+ },
99
+ "CHANNEL_SLIDER.ARROW_LEFT": {
100
+ actions: ["decrementChannel"],
101
+ },
102
+ "CHANNEL_SLIDER.ARROW_RIGHT": {
103
+ actions: ["incrementChannel"],
104
+ },
105
+ "CHANNEL_SLIDER.ARROW_UP": {
106
+ actions: ["incrementChannel"],
107
+ },
108
+ "CHANNEL_SLIDER.ARROW_DOWN": {
109
+ actions: ["decrementChannel"],
110
+ },
111
+ "CHANNEL_SLIDER.PAGE_UP": {
112
+ actions: ["incrementChannel"],
113
+ },
114
+ "CHANNEL_SLIDER.PAGE_DOWN": {
115
+ actions: ["decrementChannel"],
116
+ },
117
+ "CHANNEL_SLIDER.HOME": {
118
+ actions: ["setChannelToMin"],
119
+ },
120
+ "CHANNEL_SLIDER.END": {
121
+ actions: ["setChannelToMax"],
122
+ },
123
+ "CHANNEL_INPUT.FOCUS": {
124
+ actions: ["setActiveChannel"],
125
+ },
126
+ "CHANNEL_INPUT.CHANGE": {
127
+ actions: ["setChannelColorFromInput"],
128
+ },
129
+ "CHANNEL_INPUT.BLUR": [
130
+ {
131
+ guard: "isTextField",
132
+ target: "idle",
133
+ actions: ["setChannelColorFromInput"],
134
+ },
135
+ { target: "idle" },
136
+ ],
137
+ "CHANNEL_SLIDER.BLUR": {
138
+ target: "idle",
139
+ },
140
+ "AREA.BLUR": {
141
+ target: "idle",
142
+ },
143
+ },
144
+ },
145
+
146
+ dragging: {
147
+ exit: ["clearActiveChannel"],
148
+ activities: ["trackPointerMove", "disableTextSelection"],
149
+ on: {
150
+ "AREA.POINTER_MOVE": {
151
+ actions: ["setAreaColorFromPoint"],
152
+ },
153
+ "AREA.POINTER_UP": {
154
+ target: "focused",
155
+ actions: ["invokeOnChangeEnd"],
156
+ },
157
+ "CHANNEL_SLIDER.POINTER_MOVE": {
158
+ actions: ["setChannelColorFromPoint"],
159
+ },
160
+ "CHANNEL_SLIDER.POINTER_UP": {
161
+ target: "focused",
162
+ actions: ["invokeOnChangeEnd"],
163
+ },
164
+ },
165
+ },
166
+ },
167
+ },
168
+ {
169
+ guards: {
170
+ isTextField: (_ctx, evt) => !!evt.isTextField,
171
+ },
172
+ activities: {
173
+ trackPointerMove(ctx, _evt, { send }) {
174
+ return trackPointerMove(dom.getDoc(ctx), {
175
+ onPointerMove({ point }) {
176
+ const type = ctx.activeId === "area" ? "AREA.POINTER_MOVE" : "CHANNEL_SLIDER.POINTER_MOVE"
177
+ send({ type, point })
178
+ },
179
+ onPointerUp() {
180
+ const type = ctx.activeId === "area" ? "AREA.POINTER_UP" : "CHANNEL_SLIDER.POINTER_UP"
181
+ send({ type })
182
+ },
183
+ })
184
+ },
185
+ disableTextSelection(ctx) {
186
+ return disableTextSelection({ doc: dom.getDoc(ctx), target: dom.getContentEl(ctx) })
187
+ },
188
+ },
189
+ actions: {
190
+ openEyeDropper(ctx) {
191
+ const isSupported = "EyeDropper" in dom.getWin(ctx)
192
+ if (!isSupported) return
193
+ const win = dom.getWin(ctx) as any
194
+ const picker = new win.EyeDropper()
195
+ picker
196
+ .open()
197
+ .then(({ sRGBHex }: { sRGBHex: string }) => {
198
+ const format = ctx.valueAsColor.getColorSpace()
199
+ const color = parseColor(sRGBHex).toFormat(format)
200
+ setColor(ctx, color)
201
+ ctx.onChangeEnd?.({ value: ctx.value, valueAsColor: color })
202
+ })
203
+ .catch(() => void 0)
204
+ },
205
+ setActiveChannel(ctx, evt) {
206
+ ctx.activeId = evt.id
207
+ if (evt.channel) {
208
+ ctx.activeChannel = evt.channel
209
+ }
210
+ if (evt.orientation) {
211
+ ctx.activeOrientation = evt.orientation
212
+ }
213
+ },
214
+ clearActiveChannel(ctx) {
215
+ ctx.activeChannel = null
216
+ ctx.activeId = null
217
+ ctx.activeOrientation = null
218
+ },
219
+ setAreaColorFromPoint(ctx, evt) {
220
+ const { xChannel, yChannel } = evt.channel || ctx.activeChannel
221
+
222
+ const percent = dom.getAreaValueFromPoint(ctx, evt.point)
223
+ const { getColorFromPoint } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
224
+ const color = getColorFromPoint(percent.x, percent.y)
225
+
226
+ if (!color) return
227
+ setColor(ctx, color)
228
+ },
229
+ setChannelColorFromPoint(ctx, evt) {
230
+ const channel = evt.channel || ctx.activeId
231
+ const percent = dom.getChannelSliderValueFromPoint(ctx, evt.point, channel)
232
+
233
+ const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(channel)
234
+ const orientation = ctx.activeOrientation || "horizontal"
235
+
236
+ const point = orientation === "horizontal" ? percent.x : percent.y
237
+ const channelValue = getPercentValue(point, minValue, maxValue, step)
238
+
239
+ const value = snapValueToStep(channelValue - step, minValue, maxValue, step)
240
+ const newColor = ctx.valueAsColor.withChannelValue(channel, value)
241
+
242
+ setColor(ctx, newColor)
243
+ },
244
+ setValue(ctx, evt) {
245
+ setColor(ctx, evt.value)
246
+ },
247
+ setValueAsColor(ctx) {
248
+ try {
249
+ const color = parseColor(ctx.value)
250
+ if (color.isEqual(ctx.valueAsColor)) return
251
+ ctx.valueAsColor = color
252
+ } catch {}
253
+ },
254
+ syncChannelInputs(ctx) {
255
+ const inputs = dom.getChannelInputEls(ctx)
256
+ inputs.forEach((input) => {
257
+ const channel = input.getAttribute("data-channel") as ExtendedColorChannel | null
258
+ if (!channel) return
259
+ const value = getChannelInputValue(ctx.valueAsColor, channel)
260
+ input.value = value.toString()
261
+ })
262
+ },
263
+ setChannelColorFromInput(ctx, evt) {
264
+ const { channel, isTextField, value } = evt
265
+ try {
266
+ const format = ctx.valueAsColor.getColorSpace()
267
+
268
+ const newColor = isTextField
269
+ ? parseColor(value).toFormat(format)
270
+ : ctx.valueAsColor.withChannelValue(channel, value)
271
+
272
+ setColor(ctx, newColor)
273
+ //
274
+ } catch {
275
+ // reset input value
276
+ const input = dom.getChannelInputEl(ctx, channel)
277
+ if (!input) return
278
+ input.value = getChannelInputValue(ctx.valueAsColor, channel)
279
+ }
280
+ },
281
+
282
+ incrementChannel(ctx, evt) {
283
+ const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(evt.channel)
284
+ const channelValue = ctx.valueAsColor.getChannelValue(evt.channel)
285
+ const value = snapValueToStep(channelValue + evt.step, minValue, maxValue, step)
286
+ const newColor = ctx.valueAsColor.withChannelValue(evt.channel, clampValue(value, minValue, maxValue))
287
+ setColor(ctx, newColor)
288
+ },
289
+ decrementChannel(ctx, evt) {
290
+ const { minValue, maxValue, step } = ctx.valueAsColor.getChannelRange(evt.channel)
291
+ const channelValue = ctx.valueAsColor.getChannelValue(evt.channel)
292
+ const value = snapValueToStep(channelValue - evt.step, minValue, maxValue, step)
293
+ const newColor = ctx.valueAsColor.withChannelValue(evt.channel, clampValue(value, minValue, maxValue))
294
+ setColor(ctx, newColor)
295
+ },
296
+
297
+ incrementXChannel(ctx, evt) {
298
+ const { xChannel, yChannel } = evt.channel
299
+ const { incrementX } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
300
+ const newColor = ctx.valueAsColor.withChannelValue(xChannel, incrementX(evt.step))
301
+ setColor(ctx, newColor)
302
+ },
303
+ decrementXChannel(ctx, evt) {
304
+ const { xChannel, yChannel } = evt.channel
305
+ const { decrementX } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
306
+ const newColor = ctx.valueAsColor.withChannelValue(xChannel, decrementX(evt.step))
307
+ setColor(ctx, newColor)
308
+ },
309
+
310
+ incrementYChannel(ctx, evt) {
311
+ const { xChannel, yChannel } = evt.channel
312
+ const { incrementY } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
313
+ const newColor = ctx.valueAsColor.withChannelValue(yChannel, incrementY(evt.step))
314
+ setColor(ctx, newColor)
315
+ },
316
+ decrementYChannel(ctx, evt) {
317
+ const { xChannel, yChannel } = evt.channel
318
+ const { decrementY } = getChannelDetails(ctx.valueAsColor, xChannel, yChannel)
319
+ const newColor = ctx.valueAsColor.withChannelValue(yChannel, decrementY(evt.step))
320
+ setColor(ctx, newColor)
321
+ },
322
+
323
+ setChannelToMax(ctx, evt) {
324
+ const { maxValue } = ctx.valueAsColor.getChannelRange(evt.channel)
325
+ const newColor = ctx.valueAsColor.withChannelValue(evt.channel, maxValue)
326
+ setColor(ctx, newColor)
327
+ },
328
+ setChannelToMin(ctx, evt) {
329
+ const { minValue } = ctx.valueAsColor.getChannelRange(evt.channel)
330
+ const newColor = ctx.valueAsColor.withChannelValue(evt.channel, minValue)
331
+ setColor(ctx, newColor)
332
+ },
333
+
334
+ invokeOnChangeEnd(ctx) {
335
+ ctx.onChangeEnd?.({ value: ctx.value, valueAsColor: ctx.valueAsColor })
336
+ },
337
+ invokeOnChange(ctx) {
338
+ ctx.onChange?.({ value: ctx.value, valueAsColor: ctx.valueAsColor })
339
+ },
340
+ focusAreaThumb(ctx) {
341
+ raf(() => {
342
+ dom.getAreaThumbEl(ctx)?.focus({ preventScroll: true })
343
+ })
344
+ },
345
+ focusChannelThumb(ctx, evt) {
346
+ raf(() => {
347
+ dom.getChannelSliderThumbEl(ctx, evt.channel)?.focus({ preventScroll: true })
348
+ })
349
+ },
350
+ },
351
+ },
352
+ )
353
+ }
354
+
355
+ const setColor = (ctx: MachineContext, color: Color) => {
356
+ ctx.value = color.toString("css")
357
+ ctx.valueAsColor = color
358
+ }
@@ -0,0 +1,113 @@
1
+ import { Color, ColorAxes, ColorChannel } from "@zag-js/color-utils"
2
+ import type { StateMachine as S } from "@zag-js/core"
3
+ import type { CommonProperties, Context, Orientation, RequiredBy } from "@zag-js/types"
4
+
5
+ export type ColorChannelProps = {
6
+ channel: ColorChannel
7
+ orientation?: Orientation
8
+ }
9
+
10
+ export type ExtendedColorChannel = ColorChannel | "hex" | "css"
11
+
12
+ export type ColorChannelInputProps = {
13
+ channel: ExtendedColorChannel
14
+ orientation?: Orientation
15
+ }
16
+
17
+ export type ColorAreaProps = {
18
+ xChannel: ColorChannel
19
+ yChannel: ColorChannel
20
+ }
21
+
22
+ export type ColorSwatchProps = {
23
+ readOnly?: boolean
24
+ value: string | Color
25
+ }
26
+
27
+ type ChangeDetails = {
28
+ value: string
29
+ valueAsColor: Color
30
+ }
31
+
32
+ type ElementIds = Partial<{
33
+ content: string
34
+ area: string
35
+ areaGradient: string
36
+ areaThumb: string
37
+ channelInput(id: string): string
38
+ channelSliderTrack(id: ColorChannel): string
39
+ channelSliderThumb(id: ColorChannel): string
40
+ }>
41
+
42
+ type PublicContext = CommonProperties & {
43
+ /**
44
+ * The ids of the elements in the color picker. Useful for composition.
45
+ */
46
+ ids?: ElementIds
47
+ /**
48
+ * The direction of the color picker
49
+ */
50
+ dir: "ltr" | "rtl"
51
+ /**
52
+ * The current color value
53
+ */
54
+ value: string
55
+ /**
56
+ * Whether the color picker is disabled
57
+ */
58
+ disabled?: boolean
59
+ /**
60
+ * Whether the color picker is read-only
61
+ */
62
+ readOnly?: boolean
63
+ /**
64
+ * Handler that is called when the value changes, as the user drags.
65
+ */
66
+ onChange?: (details: ChangeDetails) => void
67
+ /**
68
+ * Handler that is called when the user stops dragging.
69
+ */
70
+ onChangeEnd?: (details: ChangeDetails) => void
71
+ }
72
+
73
+ type PrivateContext = Context<{
74
+ /**
75
+ * The id of the thumb that is currently being dragged
76
+ */
77
+ activeId: string | null
78
+ /**
79
+ * The color value as a Color object
80
+ */
81
+ valueAsColor: Color
82
+ /**
83
+ * The channel that is currently being interacted with
84
+ */
85
+ activeChannel: Partial<ColorAxes> | null
86
+ /**
87
+ * The orientation of the channel that is currently being interacted with
88
+ */
89
+ activeOrientation: Orientation | null
90
+ }>
91
+
92
+ type ComputedContext = Readonly<{
93
+ /**
94
+ * Whether the color picker is in RTL mode
95
+ */
96
+ isRtl: boolean
97
+ /**
98
+ * Whether the color picker is interactive
99
+ */
100
+ isInteractive: boolean
101
+ }>
102
+
103
+ export type UserDefinedContext = RequiredBy<PublicContext, "id">
104
+
105
+ export type MachineContext = PublicContext & PrivateContext & ComputedContext
106
+
107
+ export type MachineState = {
108
+ value: "idle" | "focused" | "dragging"
109
+ }
110
+
111
+ export type State = S.State<MachineContext, MachineState>
112
+
113
+ export type Send = S.Send<S.AnyEventObject>
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { anatomy } from "./color-picker.anatomy"
2
+ export { connect } from "./color-picker.connect"
3
+ export { machine } from "./color-picker.machine"
4
+ export type {
5
+ ColorAreaProps,
6
+ ColorChannelProps,
7
+ ColorSwatchProps,
8
+ UserDefinedContext as Context,
9
+ } from "./color-picker.types"
@@ -0,0 +1,139 @@
1
+ export const generateRGB_R = (orientation: [string, string], dir: boolean, zValue: number) => {
2
+ const maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`
3
+ const result = {
4
+ areaStyles: {
5
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,0),rgb(${zValue},255,0))`,
6
+ },
7
+ areaGradientStyles: {
8
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,255),rgb(${zValue},255,255))`,
9
+ WebkitMaskImage: maskImage,
10
+ maskImage,
11
+ },
12
+ }
13
+ return result
14
+ }
15
+
16
+ export const generateRGB_G = (orientation: [string, string], dir: boolean, zValue: number) => {
17
+ const maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`
18
+ const result = {
19
+ areaStyles: {
20
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},0),rgb(255,${zValue},0))`,
21
+ },
22
+ areaGradientStyles: {
23
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},255),rgb(255,${zValue},255))`,
24
+ WebkitMaskImage: maskImage,
25
+ maskImage,
26
+ },
27
+ }
28
+ return result
29
+ }
30
+
31
+ export const generateRGB_B = (orientation: [string, string], dir: boolean, zValue: number) => {
32
+ const maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`
33
+ const result = {
34
+ areaStyles: {
35
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,0,${zValue}),rgb(255,0,${zValue}))`,
36
+ },
37
+ areaGradientStyles: {
38
+ backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,255,${zValue}),rgb(255,255,${zValue}))`,
39
+ WebkitMaskImage: maskImage,
40
+ maskImage,
41
+ },
42
+ }
43
+ return result
44
+ }
45
+
46
+ export const generateHSL_H = (orientation: [string, string], dir: boolean, zValue: number) => {
47
+ const result = {
48
+ areaStyles: {},
49
+ areaGradientStyles: {
50
+ background: [
51
+ `linear-gradient(to ${
52
+ orientation[Number(dir)]
53
+ }, hsla(0,0%,0%,1) 0%, hsla(0,0%,0%,0) 50%, hsla(0,0%,100%,0) 50%, hsla(0,0%,100%,1) 100%)`,
54
+ `linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,50%),hsla(0,0%,50%,0))`,
55
+ `hsl(${zValue}, 100%, 50%)`,
56
+ ].join(","),
57
+ },
58
+ }
59
+ return result
60
+ }
61
+
62
+ export const generateHSL_S = (orientation: [string, string], dir: boolean, alphaValue: number) => {
63
+ const result = {
64
+ areaStyles: {},
65
+ areaGradientStyles: {
66
+ background: [
67
+ `linear-gradient(to ${
68
+ orientation[Number(!dir)]
69
+ }, hsla(0,0%,0%,${alphaValue}) 0%, hsla(0,0%,0%,0) 50%, hsla(0,0%,100%,0) 50%, hsla(0,0%,100%,${alphaValue}) 100%)`,
70
+ `linear-gradient(to ${
71
+ orientation[Number(dir)]
72
+ },hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
73
+ "hsl(0, 0%, 50%)",
74
+ ].join(","),
75
+ },
76
+ }
77
+ return result
78
+ }
79
+
80
+ export const generateHSL_L = (orientation: [string, string], dir: boolean, zValue: number) => {
81
+ const result = {
82
+ areaStyles: {},
83
+ areaGradientStyles: {
84
+ backgroundImage: [
85
+ `linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,${zValue}%),hsla(0,0%,${zValue}%,0))`,
86
+ `linear-gradient(to ${
87
+ orientation[Number(dir)]
88
+ },hsl(0,100%,${zValue}%),hsl(60,100%,${zValue}%),hsl(120,100%,${zValue}%),hsl(180,100%,${zValue}%),hsl(240,100%,${zValue}%),hsl(300,100%,${zValue}%),hsl(360,100%,${zValue}%))`,
89
+ ].join(","),
90
+ },
91
+ }
92
+ return result
93
+ }
94
+
95
+ export const generateHSB_H = (orientation: [string, string], dir: boolean, zValue: number) => {
96
+ const result = {
97
+ areaStyles: {},
98
+ areaGradientStyles: {
99
+ background: [
100
+ `linear-gradient(to ${orientation[Number(dir)]},hsl(0,0%,0%),hsla(0,0%,0%,0))`,
101
+ `linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,100%),hsla(0,0%,100%,0))`,
102
+ `hsl(${zValue}, 100%, 50%)`,
103
+ ].join(","),
104
+ },
105
+ }
106
+ return result
107
+ }
108
+
109
+ export const generateHSB_S = (orientation: [string, string], dir: boolean, alphaValue: number) => {
110
+ const result = {
111
+ areaStyles: {},
112
+ areaGradientStyles: {
113
+ background: [
114
+ `linear-gradient(to ${orientation[Number(!dir)]},hsla(0,0%,0%,${alphaValue}),hsla(0,0%,0%,0))`,
115
+ `linear-gradient(to ${
116
+ orientation[Number(dir)]
117
+ },hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
118
+ `linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,0%),hsl(0,0%,100%))`,
119
+ ].join(","),
120
+ },
121
+ }
122
+ return result
123
+ }
124
+
125
+ export const generateHSB_B = (orientation: [string, string], dir: boolean, alphaValue: number) => {
126
+ const result = {
127
+ areaStyles: {},
128
+ areaGradientStyles: {
129
+ background: [
130
+ `linear-gradient(to ${orientation[Number(!dir)]},hsla(0,0%,100%,${alphaValue}),hsla(0,0%,100%,0))`,
131
+ `linear-gradient(to ${
132
+ orientation[Number(dir)]
133
+ },hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
134
+ "#000",
135
+ ].join(","),
136
+ },
137
+ }
138
+ return result
139
+ }
@@ -0,0 +1,60 @@
1
+ import { Color, ColorChannel } from "@zag-js/color-utils"
2
+ import { snapValueToStep, getPercentValue } from "@zag-js/numeric-range"
3
+
4
+ export function getChannelDetails(color: Color, xChannel: ColorChannel, yChannel: ColorChannel) {
5
+ const channels = color.getColorSpaceAxes({ xChannel, yChannel })
6
+
7
+ const xChannelRange = color.getChannelRange(channels.xChannel)
8
+ const yChannelRange = color.getChannelRange(channels.yChannel)
9
+
10
+ const { minValue: minValueX, maxValue: maxValueX, step: stepX, pageSize: pageSizeX } = xChannelRange
11
+ const { minValue: minValueY, maxValue: maxValueY, step: stepY, pageSize: pageSizeY } = yChannelRange
12
+
13
+ const xValue = color.getChannelValue(channels.xChannel)
14
+ const yValue = color.getChannelValue(channels.yChannel)
15
+
16
+ return {
17
+ channels,
18
+ xChannelStep: stepX,
19
+ yChannelStep: stepY,
20
+ xChannelPageStep: pageSizeX,
21
+ yChannelPageStep: pageSizeY,
22
+ xValue,
23
+ yValue,
24
+ getThumbPosition() {
25
+ let x = (xValue - minValueX) / (maxValueX - minValueX)
26
+ let y = 1 - (yValue - minValueY) / (maxValueY - minValueY)
27
+ return { x, y }
28
+ },
29
+ incrementX(stepSize: number) {
30
+ return xValue + stepSize > maxValueX ? maxValueX : snapValueToStep(xValue + stepSize, minValueX, maxValueX, stepX)
31
+ },
32
+ incrementY(stepSize: number) {
33
+ return yValue + stepSize > maxValueY ? maxValueY : snapValueToStep(yValue + stepSize, minValueY, maxValueY, stepY)
34
+ },
35
+ decrementX(stepSize: number) {
36
+ return snapValueToStep(xValue - stepSize, minValueX, maxValueX, stepX)
37
+ },
38
+ decrementY(stepSize: number) {
39
+ return snapValueToStep(yValue - stepSize, minValueY, maxValueY, stepY)
40
+ },
41
+ getColorFromPoint(x: number, y: number) {
42
+ let newXValue = getPercentValue(x, minValueX, maxValueX, stepX)
43
+ let newYValue = getPercentValue(1 - y, minValueY, maxValueY, stepY)
44
+
45
+ let newColor: Color | undefined
46
+
47
+ if (newXValue !== xValue) {
48
+ newXValue = snapValueToStep(newXValue, minValueX, maxValueX, stepX)
49
+ newColor = color.withChannelValue(channels.xChannel, newXValue)
50
+ }
51
+
52
+ if (newYValue !== yValue) {
53
+ newYValue = snapValueToStep(newYValue, minValueY, maxValueY, stepY)
54
+ newColor = (newColor || color).withChannelValue(channels.yChannel, newYValue)
55
+ }
56
+
57
+ return newColor
58
+ },
59
+ }
60
+ }
@@ -0,0 +1,20 @@
1
+ import { Color, ColorChannel, parseColor } from "@zag-js/color-utils"
2
+
3
+ export function getChannelDisplayColor(color: Color, channel: ColorChannel) {
4
+ switch (channel) {
5
+ case "hue":
6
+ return parseColor(`hsl(${color.getChannelValue("hue")}, 100%, 50%)`)
7
+ case "lightness":
8
+ case "brightness":
9
+ case "saturation":
10
+ case "red":
11
+ case "green":
12
+ case "blue":
13
+ return color.withChannelValue("alpha", 1)
14
+ case "alpha": {
15
+ return color
16
+ }
17
+ default:
18
+ throw new Error("Unknown color channel: " + channel)
19
+ }
20
+ }