@xsolla/xui-gradient-picker 0.204.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,1033 @@
1
+ // src/GradientPicker.tsx
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ // ../../foundation/primitives-native/src/Box.tsx
5
+ import {
6
+ View,
7
+ Pressable,
8
+ Image
9
+ } from "react-native";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var roleToAccessibilityRole = {
12
+ button: "button",
13
+ link: "link",
14
+ img: "image",
15
+ image: "image",
16
+ summary: "summary",
17
+ switch: "switch",
18
+ checkbox: "checkbox",
19
+ radio: "radio",
20
+ heading: "header",
21
+ text: "text",
22
+ search: "search",
23
+ alert: "alert",
24
+ none: "none",
25
+ presentation: "none",
26
+ progressbar: "progressbar",
27
+ scrollbar: "scrollbar",
28
+ adjustable: "adjustable",
29
+ tab: "tab",
30
+ menu: "menu",
31
+ menuitem: "menuitem",
32
+ list: "list",
33
+ timer: "timer"
34
+ };
35
+ var toGapValue = (gap) => {
36
+ if (gap === void 0) return void 0;
37
+ if (typeof gap === "number") return gap;
38
+ const pxMatch = /^(\d+(?:\.\d+)?)px$/.exec(gap);
39
+ if (pxMatch) return Number(pxMatch[1]);
40
+ const asNumber = Number(gap);
41
+ return Number.isFinite(asNumber) ? asNumber : void 0;
42
+ };
43
+ var Box = ({
44
+ children,
45
+ onPress,
46
+ onLayout,
47
+ onMoveShouldSetResponder,
48
+ onResponderGrant,
49
+ onResponderMove,
50
+ onResponderRelease,
51
+ onResponderTerminate,
52
+ backgroundColor,
53
+ borderColor,
54
+ borderWidth,
55
+ borderBottomWidth,
56
+ borderBottomColor,
57
+ borderTopWidth,
58
+ borderTopColor,
59
+ borderLeftWidth,
60
+ borderLeftColor,
61
+ borderRightWidth,
62
+ borderRightColor,
63
+ borderRadius,
64
+ borderStyle,
65
+ height,
66
+ padding,
67
+ paddingHorizontal,
68
+ paddingVertical,
69
+ margin,
70
+ marginTop,
71
+ marginBottom,
72
+ marginLeft,
73
+ marginRight,
74
+ flexDirection,
75
+ flexWrap,
76
+ alignItems,
77
+ justifyContent,
78
+ alignSelf,
79
+ position,
80
+ top,
81
+ bottom,
82
+ left,
83
+ right,
84
+ width,
85
+ minWidth,
86
+ minHeight,
87
+ maxWidth,
88
+ maxHeight,
89
+ flex,
90
+ flexShrink,
91
+ gap,
92
+ overflow,
93
+ opacity,
94
+ zIndex,
95
+ hoverStyle,
96
+ pressStyle,
97
+ style,
98
+ disabled,
99
+ "data-testid": dataTestId,
100
+ testID,
101
+ as,
102
+ src,
103
+ alt,
104
+ ...rest
105
+ }) => {
106
+ const getContainerStyle = (pressed) => ({
107
+ backgroundColor: pressed && pressStyle?.backgroundColor ? pressStyle.backgroundColor : backgroundColor,
108
+ borderColor,
109
+ borderWidth,
110
+ borderBottomWidth,
111
+ borderBottomColor,
112
+ borderTopWidth,
113
+ borderTopColor,
114
+ borderLeftWidth,
115
+ borderLeftColor,
116
+ borderRightWidth,
117
+ borderRightColor,
118
+ borderRadius,
119
+ borderStyle,
120
+ overflow,
121
+ opacity: opacity ?? (disabled ? 0.5 : void 0),
122
+ zIndex,
123
+ height,
124
+ width,
125
+ minWidth,
126
+ minHeight,
127
+ maxWidth,
128
+ maxHeight,
129
+ padding,
130
+ paddingHorizontal,
131
+ paddingVertical,
132
+ margin,
133
+ marginTop,
134
+ marginBottom,
135
+ marginLeft,
136
+ marginRight,
137
+ flexDirection,
138
+ flexWrap,
139
+ alignItems,
140
+ justifyContent,
141
+ alignSelf,
142
+ position,
143
+ top,
144
+ bottom,
145
+ left,
146
+ right,
147
+ flex,
148
+ flexShrink,
149
+ gap: toGapValue(gap),
150
+ ...style
151
+ });
152
+ const finalTestID = dataTestId || testID;
153
+ const {
154
+ role,
155
+ tabIndex,
156
+ onKeyDown,
157
+ onKeyUp,
158
+ "aria-label": ariaLabel,
159
+ "aria-labelledby": _ariaLabelledBy,
160
+ "aria-current": _ariaCurrent,
161
+ "aria-disabled": ariaDisabled,
162
+ "aria-live": ariaLive,
163
+ "aria-busy": ariaBusy,
164
+ className,
165
+ "data-testid": _dataTestId,
166
+ cursor: _cursor,
167
+ type: _type,
168
+ ...nativeRest
169
+ } = rest;
170
+ const accessibilityRole = typeof role === "string" && roleToAccessibilityRole[role] || (as === "button" ? "button" : void 0);
171
+ const isDisabled = Boolean(disabled || ariaDisabled);
172
+ const accessibilityState = {
173
+ disabled: isDisabled || void 0,
174
+ busy: Boolean(ariaBusy) || void 0
175
+ };
176
+ const accessibilityLiveRegion = ariaLive === "polite" || ariaLive === "assertive" ? ariaLive : void 0;
177
+ if (as === "img" && src) {
178
+ const imageStyle = {
179
+ width,
180
+ height,
181
+ borderRadius,
182
+ position,
183
+ top,
184
+ bottom,
185
+ left,
186
+ right,
187
+ ...style
188
+ };
189
+ return /* @__PURE__ */ jsx(
190
+ Image,
191
+ {
192
+ source: { uri: src },
193
+ style: imageStyle,
194
+ testID: finalTestID,
195
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : alt,
196
+ resizeMode: "cover",
197
+ ...nativeRest
198
+ }
199
+ );
200
+ }
201
+ if (onPress || as === "button") {
202
+ return /* @__PURE__ */ jsx(
203
+ Pressable,
204
+ {
205
+ onPress: !isDisabled && onPress ? onPress : void 0,
206
+ disabled: isDisabled,
207
+ onLayout,
208
+ onMoveShouldSetResponder,
209
+ onResponderGrant,
210
+ onResponderMove,
211
+ onResponderRelease,
212
+ onResponderTerminate,
213
+ style: ({ pressed }) => getContainerStyle(pressed),
214
+ testID: finalTestID,
215
+ accessibilityRole,
216
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
217
+ accessibilityState,
218
+ accessibilityLiveRegion,
219
+ ...nativeRest,
220
+ children
221
+ }
222
+ );
223
+ }
224
+ return /* @__PURE__ */ jsx(
225
+ View,
226
+ {
227
+ style: getContainerStyle(),
228
+ testID: finalTestID,
229
+ onLayout,
230
+ onMoveShouldSetResponder,
231
+ onResponderGrant,
232
+ onResponderMove,
233
+ onResponderRelease,
234
+ onResponderTerminate,
235
+ accessibilityRole,
236
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
237
+ accessibilityState,
238
+ accessibilityLiveRegion,
239
+ ...nativeRest,
240
+ children
241
+ }
242
+ );
243
+ };
244
+
245
+ // ../../foundation/primitives-native/src/Text.tsx
246
+ import {
247
+ Text as RNText,
248
+ StyleSheet
249
+ } from "react-native";
250
+ import { jsx as jsx2 } from "react/jsx-runtime";
251
+ var roleMap = {
252
+ alert: "alert",
253
+ heading: "header",
254
+ button: "button",
255
+ link: "link",
256
+ text: "text"
257
+ };
258
+ var parseNumericValue = (value) => {
259
+ if (value === void 0) return void 0;
260
+ if (typeof value === "number") return value;
261
+ const parsed = parseFloat(value);
262
+ return isNaN(parsed) ? void 0 : parsed;
263
+ };
264
+ var Text = ({
265
+ children,
266
+ color,
267
+ fontSize,
268
+ fontWeight,
269
+ fontFamily,
270
+ textAlign,
271
+ lineHeight,
272
+ numberOfLines,
273
+ id,
274
+ role,
275
+ testID,
276
+ "data-testid": dataTestId,
277
+ style: styleProp,
278
+ ...props
279
+ }) => {
280
+ let resolvedFontFamily = fontFamily ? fontFamily.split(",")[0].replace(/['"]/g, "").trim() : void 0;
281
+ if (resolvedFontFamily === "Pilat Wide" || resolvedFontFamily === "Pilat Wide Bold" || resolvedFontFamily === "Aktiv Grotesk") {
282
+ resolvedFontFamily = void 0;
283
+ }
284
+ const incomingStyle = StyleSheet.flatten(styleProp);
285
+ const baseStyle = {
286
+ color: color ?? incomingStyle?.color,
287
+ fontSize: typeof fontSize === "number" ? fontSize : void 0,
288
+ fontWeight,
289
+ fontFamily: resolvedFontFamily,
290
+ textDecorationLine: props.textDecoration,
291
+ textAlign: textAlign ?? incomingStyle?.textAlign,
292
+ lineHeight: parseNumericValue(lineHeight ?? incomingStyle?.lineHeight),
293
+ marginTop: parseNumericValue(
294
+ incomingStyle?.marginTop
295
+ ),
296
+ marginBottom: parseNumericValue(
297
+ incomingStyle?.marginBottom
298
+ )
299
+ };
300
+ const accessibilityRole = role ? roleMap[role] : void 0;
301
+ return /* @__PURE__ */ jsx2(
302
+ RNText,
303
+ {
304
+ style: baseStyle,
305
+ numberOfLines,
306
+ testID: dataTestId || testID || id,
307
+ accessibilityRole,
308
+ children
309
+ }
310
+ );
311
+ };
312
+
313
+ // ../../foundation/primitives-native/src/index.tsx
314
+ var isWeb = false;
315
+
316
+ // src/GradientPicker.tsx
317
+ import { useResolvedTheme as useResolvedTheme2 } from "@xsolla/xui-core";
318
+ import { Input } from "@xsolla/xui-input";
319
+ import { InputColor } from "@xsolla/xui-input-color";
320
+ import { Select } from "@xsolla/xui-select";
321
+ import { IconButton } from "@xsolla/xui-button";
322
+ import { ColorPicker } from "@xsolla/xui-color-picker";
323
+
324
+ // src/GradientStopMarker.tsx
325
+ import { useResolvedTheme } from "@xsolla/xui-core";
326
+
327
+ // src/utils.ts
328
+ var MIN_POSITION = 0;
329
+ var MAX_POSITION = 100;
330
+ var clampPosition = (position) => {
331
+ if (!Number.isFinite(position)) return MIN_POSITION;
332
+ return Math.min(MAX_POSITION, Math.max(MIN_POSITION, Math.round(position)));
333
+ };
334
+ var clampOpacity = (opacity) => {
335
+ if (!Number.isFinite(opacity)) return 100;
336
+ return Math.min(100, Math.max(0, Math.round(opacity)));
337
+ };
338
+ var normalizeHex = (hex) => {
339
+ const raw = String(hex ?? "").trim().replace(/^#/, "");
340
+ if (/^[0-9a-fA-F]{3}$/.test(raw)) {
341
+ return `#${raw.split("").map((char) => char + char).join("")}`.toUpperCase();
342
+ }
343
+ if (/^[0-9a-fA-F]{6}$/.test(raw)) return `#${raw}`.toUpperCase();
344
+ if (/^[0-9a-fA-F]{8}$/.test(raw)) return `#${raw.slice(0, 6)}`.toUpperCase();
345
+ return "#000000";
346
+ };
347
+ var hexToRgb = (hex) => {
348
+ const normalized = normalizeHex(hex).slice(1);
349
+ return {
350
+ r: parseInt(normalized.slice(0, 2), 16),
351
+ g: parseInt(normalized.slice(2, 4), 16),
352
+ b: parseInt(normalized.slice(4, 6), 16)
353
+ };
354
+ };
355
+ var rgbToHex = (r, g, b) => {
356
+ const toChannel = (channel) => Math.min(255, Math.max(0, Math.round(channel))).toString(16).padStart(2, "0");
357
+ return `#${toChannel(r)}${toChannel(g)}${toChannel(b)}`.toUpperCase();
358
+ };
359
+ var sortStops = (stops) => stops.map((stop, index) => ({ stop, index })).sort(
360
+ (a, b) => a.stop.position === b.stop.position ? a.index - b.index : a.stop.position - b.stop.position
361
+ ).map(({ stop }) => stop);
362
+ var stopToCssColor = (stop) => {
363
+ const opacity = clampOpacity(stop.opacity);
364
+ if (opacity >= 100) return normalizeHex(stop.color);
365
+ const { r, g, b } = hexToRgb(stop.color);
366
+ return `rgba(${r}, ${g}, ${b}, ${Number((opacity / 100).toFixed(2))})`;
367
+ };
368
+ var stopsToCssStopList = (stops) => {
369
+ if (stops.length === 0) return "transparent 0%, transparent 100%";
370
+ if (stops.length === 1) {
371
+ const color = stopToCssColor(stops[0]);
372
+ return `${color} 0%, ${color} 100%`;
373
+ }
374
+ return sortStops(stops).map((stop) => `${stopToCssColor(stop)} ${clampPosition(stop.position)}%`).join(", ");
375
+ };
376
+ var toCssGradient = (value) => {
377
+ const stopList = stopsToCssStopList(value.stops);
378
+ const angle = Number.isFinite(value.angle) ? Math.round(value.angle) : 0;
379
+ switch (value.type) {
380
+ case "radial":
381
+ return `radial-gradient(circle, ${stopList})`;
382
+ case "angular":
383
+ return `conic-gradient(from ${angle}deg, ${stopList})`;
384
+ case "linear":
385
+ default:
386
+ return `linear-gradient(${angle}deg, ${stopList})`;
387
+ }
388
+ };
389
+ var toPreviewCssGradient = (value) => `linear-gradient(90deg, ${stopsToCssStopList(value.stops)})`;
390
+ var supportsAngle = (value) => value === "linear" || value === "angular";
391
+ var interpolateColorAt = (stops, position) => {
392
+ if (stops.length === 0) return { color: "#000000", opacity: 100 };
393
+ const sorted = sortStops(stops);
394
+ const target = clampPosition(position);
395
+ const first = sorted[0];
396
+ if (target <= first.position) {
397
+ return {
398
+ color: normalizeHex(first.color),
399
+ opacity: clampOpacity(first.opacity)
400
+ };
401
+ }
402
+ const last = sorted[sorted.length - 1];
403
+ if (target >= last.position) {
404
+ return {
405
+ color: normalizeHex(last.color),
406
+ opacity: clampOpacity(last.opacity)
407
+ };
408
+ }
409
+ for (let index = 0; index < sorted.length - 1; index += 1) {
410
+ const from = sorted[index];
411
+ const to = sorted[index + 1];
412
+ if (target < from.position || target > to.position) continue;
413
+ const span = to.position - from.position;
414
+ const ratio = span === 0 ? 0 : (target - from.position) / span;
415
+ const fromRgb = hexToRgb(from.color);
416
+ const toRgb = hexToRgb(to.color);
417
+ return {
418
+ color: rgbToHex(
419
+ fromRgb.r + (toRgb.r - fromRgb.r) * ratio,
420
+ fromRgb.g + (toRgb.g - fromRgb.g) * ratio,
421
+ fromRgb.b + (toRgb.b - fromRgb.b) * ratio
422
+ ),
423
+ opacity: clampOpacity(
424
+ clampOpacity(from.opacity) + (clampOpacity(to.opacity) - clampOpacity(from.opacity)) * ratio
425
+ )
426
+ };
427
+ }
428
+ return {
429
+ color: normalizeHex(last.color),
430
+ opacity: clampOpacity(last.opacity)
431
+ };
432
+ };
433
+ var createStopId = (stops) => {
434
+ const highest = stops.reduce((max, stop) => {
435
+ const match = /^stop-(\d+)$/.exec(stop.id);
436
+ return match ? Math.max(max, Number(match[1])) : max;
437
+ }, 0);
438
+ return `stop-${highest + 1}`;
439
+ };
440
+ var addStopAt = (stops, position, maxStops) => {
441
+ if (stops.length >= maxStops) return stops;
442
+ const target = clampPosition(position);
443
+ const { color, opacity } = interpolateColorAt(stops, target);
444
+ return sortStops([
445
+ ...stops,
446
+ { id: createStopId(stops), color, position: target, opacity }
447
+ ]);
448
+ };
449
+ var removeStop = (stops, id, minStops) => {
450
+ if (stops.length <= minStops) return stops;
451
+ const next = stops.filter((stop) => stop.id !== id);
452
+ return next.length === stops.length ? stops : next;
453
+ };
454
+ var updateStop = (stops, id, patch) => stops.map((stop) => {
455
+ if (stop.id !== id) return stop;
456
+ return {
457
+ ...stop,
458
+ ...patch,
459
+ color: patch.color === void 0 ? stop.color : normalizeHex(patch.color),
460
+ position: patch.position === void 0 ? stop.position : clampPosition(patch.position),
461
+ opacity: patch.opacity === void 0 ? stop.opacity : clampOpacity(patch.opacity)
462
+ };
463
+ });
464
+ var DEFAULT_GRADIENT = {
465
+ type: "linear",
466
+ angle: 0,
467
+ stops: [
468
+ { id: "stop-1", color: "#D9D9D9", position: 0, opacity: 100 },
469
+ { id: "stop-2", color: "#22A8C3", position: 100, opacity: 100 }
470
+ ]
471
+ };
472
+
473
+ // src/GradientStopMarker.tsx
474
+ import { jsx as jsx3 } from "react/jsx-runtime";
475
+ var GradientStopMarker = ({
476
+ stop,
477
+ index,
478
+ active = false,
479
+ disabled = false,
480
+ onDragStart,
481
+ onKeyDown,
482
+ themeMode,
483
+ themeProductContext
484
+ }) => {
485
+ const { theme } = useResolvedTheme({ themeMode, themeProductContext });
486
+ const size = active ? 18 : 16;
487
+ return /* @__PURE__ */ jsx3(
488
+ Box,
489
+ {
490
+ role: "slider",
491
+ tabIndex: disabled ? -1 : 0,
492
+ "aria-label": `Stop ${index} position`,
493
+ "aria-valuenow": stop.position,
494
+ "aria-valuemin": MIN_POSITION,
495
+ "aria-valuemax": MAX_POSITION,
496
+ "aria-valuetext": `${stop.position}%`,
497
+ "aria-orientation": "horizontal",
498
+ "aria-disabled": disabled || void 0,
499
+ testID: `gradient-stop-${stop.id}`,
500
+ "data-testid": `gradient-stop-${stop.id}`,
501
+ position: "absolute",
502
+ left: `${stop.position}%`,
503
+ width: size,
504
+ height: size,
505
+ borderRadius: 999,
506
+ zIndex: active ? 3 : 2,
507
+ cursor: disabled ? "not-allowed" : "grab",
508
+ onMouseDown: isWeb && !disabled ? onDragStart : void 0,
509
+ onKeyDown: disabled ? void 0 : onKeyDown,
510
+ style: {
511
+ top: "50%",
512
+ transform: "translate(-50%, -50%)",
513
+ boxSizing: "border-box",
514
+ borderStyle: "solid",
515
+ borderWidth: 3,
516
+ borderColor: theme.colors.background.primary,
517
+ backgroundColor: stopToCssColor(stop),
518
+ boxShadow: active ? "0px 0px 0px 2px rgba(7, 7, 8, 0.25)" : "0px 1px 2px rgba(7, 7, 8, 0.2), 0px 1px 3px 1px rgba(7, 7, 8, 0.1)",
519
+ transition: "width 0.1s ease, height 0.1s ease, box-shadow 0.1s ease",
520
+ outline: "none"
521
+ }
522
+ }
523
+ );
524
+ };
525
+ GradientStopMarker.displayName = "GradientStopMarker";
526
+
527
+ // src/icons.tsx
528
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
529
+ var PlusGlyph = ({
530
+ size = 18,
531
+ color = "currentColor"
532
+ }) => /* @__PURE__ */ jsx4(
533
+ "svg",
534
+ {
535
+ viewBox: "0 0 24 24",
536
+ width: size,
537
+ height: size,
538
+ fill: "none",
539
+ stroke: color,
540
+ strokeWidth: "2",
541
+ strokeLinecap: "round",
542
+ "aria-hidden": "true",
543
+ focusable: "false",
544
+ children: /* @__PURE__ */ jsx4("path", { d: "M12 5v14M5 12h14" })
545
+ }
546
+ );
547
+ var MinusGlyph = ({
548
+ size = 18,
549
+ color = "currentColor"
550
+ }) => /* @__PURE__ */ jsx4(
551
+ "svg",
552
+ {
553
+ viewBox: "0 0 24 24",
554
+ width: size,
555
+ height: size,
556
+ fill: "none",
557
+ stroke: color,
558
+ strokeWidth: "2",
559
+ strokeLinecap: "round",
560
+ "aria-hidden": "true",
561
+ focusable: "false",
562
+ children: /* @__PURE__ */ jsx4("path", { d: "M5 12h14" })
563
+ }
564
+ );
565
+ var AngleGlyph = ({
566
+ size = 16,
567
+ color = "currentColor"
568
+ }) => /* @__PURE__ */ jsx4(
569
+ "svg",
570
+ {
571
+ viewBox: "0 0 24 24",
572
+ width: size,
573
+ height: size,
574
+ fill: "none",
575
+ stroke: color,
576
+ strokeWidth: "2",
577
+ strokeLinecap: "round",
578
+ strokeLinejoin: "round",
579
+ "aria-hidden": "true",
580
+ focusable: "false",
581
+ children: /* @__PURE__ */ jsx4("path", { d: "M4 20h16L4 4z" })
582
+ }
583
+ );
584
+ var PercentGlyph = ({
585
+ size = 16,
586
+ color = "currentColor"
587
+ }) => /* @__PURE__ */ jsxs(
588
+ "svg",
589
+ {
590
+ viewBox: "0 0 24 24",
591
+ width: size,
592
+ height: size,
593
+ fill: "none",
594
+ stroke: color,
595
+ strokeWidth: "2",
596
+ strokeLinecap: "round",
597
+ "aria-hidden": "true",
598
+ focusable: "false",
599
+ children: [
600
+ /* @__PURE__ */ jsx4("circle", { cx: "6.5", cy: "6.5", r: "2.5" }),
601
+ /* @__PURE__ */ jsx4("circle", { cx: "17.5", cy: "17.5", r: "2.5" }),
602
+ /* @__PURE__ */ jsx4("path", { d: "M19 5 5 19" })
603
+ ]
604
+ }
605
+ );
606
+
607
+ // src/GradientPicker.tsx
608
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
609
+ var DEFAULT_TYPE_LABELS = {
610
+ linear: "Linear gradient",
611
+ radial: "Radial gradient",
612
+ angular: "Angular gradient"
613
+ };
614
+ var ALL_TYPES = ["linear", "radial", "angular"];
615
+ var DRAG_OFF_THRESHOLD = 32;
616
+ var TRACK_HEIGHT = 16;
617
+ var MARKER_INSET = 9;
618
+ var toHexa = (color, opacity) => {
619
+ const alpha = Math.round(clampOpacity(opacity) / 100 * 255).toString(16).padStart(2, "0").toUpperCase();
620
+ return `${normalizeHex(color)}${alpha}`;
621
+ };
622
+ var fromHexa = (value) => {
623
+ const raw = String(value ?? "").trim().replace(/^#/, "");
624
+ if (/^[0-9a-fA-F]{8}$/.test(raw)) {
625
+ return {
626
+ color: normalizeHex(raw.slice(0, 6)),
627
+ opacity: clampOpacity(parseInt(raw.slice(6, 8), 16) / 255 * 100)
628
+ };
629
+ }
630
+ return { color: normalizeHex(raw), opacity: 100 };
631
+ };
632
+ var GradientPicker = ({
633
+ value: propValue,
634
+ defaultValue,
635
+ onChange,
636
+ gradientTypes = ALL_TYPES,
637
+ typeLabels,
638
+ minStops = 1,
639
+ maxStops = 5,
640
+ disabled = false,
641
+ typeAriaLabel = "Gradient type",
642
+ angleAriaLabel = "Gradient angle",
643
+ stopsLabel = "Stops",
644
+ testID,
645
+ themeMode,
646
+ themeProductContext
647
+ }) => {
648
+ const { theme } = useResolvedTheme2({ themeMode, themeProductContext });
649
+ const contentColors = theme.colors.content;
650
+ const themeProps = { themeMode, themeProductContext };
651
+ const isControlled = propValue !== void 0;
652
+ const [innerValue, setInnerValue] = useState(
653
+ () => propValue ?? defaultValue ?? DEFAULT_GRADIENT
654
+ );
655
+ const value = isControlled ? propValue : innerValue;
656
+ const [activeStopId, setActiveStopId] = useState(null);
657
+ const [draggingStopId, setDraggingStopId] = useState(null);
658
+ const trackRef = useRef(null);
659
+ const rootRef = useRef(null);
660
+ const stops = sortStops(value.stops);
661
+ const activeStop = stops.find((stop) => stop.id === activeStopId) ?? null;
662
+ const canAdd = !disabled && stops.length < maxStops;
663
+ const canRemove = !disabled && stops.length > minStops;
664
+ const emit = useCallback(
665
+ (next) => {
666
+ if (!isControlled) setInnerValue(next);
667
+ onChange?.({ value: next, css: toCssGradient(next) });
668
+ },
669
+ [isControlled, onChange]
670
+ );
671
+ const patchStops = useCallback(
672
+ (nextStops) => {
673
+ if (nextStops === value.stops) return;
674
+ emit({ ...value, stops: nextStops });
675
+ },
676
+ [emit, value]
677
+ );
678
+ const focusMarker = useCallback((stopId) => {
679
+ if (!isWeb) return;
680
+ const node = rootRef.current?.querySelector?.(
681
+ `[data-testid="gradient-stop-${stopId}"]`
682
+ );
683
+ node?.focus?.();
684
+ }, []);
685
+ const closeColorPicker = useCallback(() => {
686
+ const previous = activeStopId;
687
+ setActiveStopId(null);
688
+ if (previous) focusMarker(previous);
689
+ }, [activeStopId, focusMarker]);
690
+ useEffect(() => {
691
+ if (!isWeb || !activeStopId) return void 0;
692
+ const onKey = (event) => {
693
+ if (event.key === "Escape") closeColorPicker();
694
+ };
695
+ document.addEventListener("keydown", onKey);
696
+ return () => document.removeEventListener("keydown", onKey);
697
+ }, [activeStopId, closeColorPicker]);
698
+ const positionFromClientX = useCallback((clientX) => {
699
+ const rect = trackRef.current?.getBoundingClientRect?.();
700
+ if (!rect || !rect.width) return 0;
701
+ return clampPosition((clientX - rect.left) / rect.width * 100);
702
+ }, []);
703
+ const addStop = (position) => {
704
+ const before = value.stops;
705
+ const after = addStopAt(before, position, maxStops);
706
+ if (after === before) return;
707
+ patchStops(after);
708
+ const added = after.find(
709
+ (stop) => !before.some((existing) => existing.id === stop.id)
710
+ );
711
+ if (added) setActiveStopId(added.id);
712
+ };
713
+ const handleTrackPress = (event) => {
714
+ if (!canAdd) return;
715
+ addStop(positionFromClientX(event.clientX));
716
+ };
717
+ const handleAddStopPress = () => {
718
+ if (!canAdd) return;
719
+ const sorted = sortStops(value.stops);
720
+ let position = 50;
721
+ let widest = -1;
722
+ for (let index = 0; index < sorted.length - 1; index += 1) {
723
+ const gap = sorted[index + 1].position - sorted[index].position;
724
+ if (gap > widest) {
725
+ widest = gap;
726
+ position = sorted[index].position + gap / 2;
727
+ }
728
+ }
729
+ addStop(position);
730
+ };
731
+ const handleRemoveStop = (stopId) => {
732
+ const after = removeStop(value.stops, stopId, minStops);
733
+ if (after === value.stops) return;
734
+ if (activeStopId === stopId) setActiveStopId(null);
735
+ patchStops(after);
736
+ };
737
+ const handleDragStart = (stop) => (event) => {
738
+ if (disabled) return;
739
+ event.preventDefault();
740
+ event.stopPropagation();
741
+ const startY = event.clientY;
742
+ setDraggingStopId(stop.id);
743
+ setActiveStopId(stop.id);
744
+ let latest = value.stops;
745
+ let removed = false;
746
+ const onMouseMove = (moveEvent) => {
747
+ if (removed) return;
748
+ if (Math.abs(moveEvent.clientY - startY) > DRAG_OFF_THRESHOLD) {
749
+ const after = removeStop(latest, stop.id, minStops);
750
+ if (after !== latest) {
751
+ latest = after;
752
+ removed = true;
753
+ patchStops(after);
754
+ }
755
+ return;
756
+ }
757
+ latest = updateStop(latest, stop.id, {
758
+ position: positionFromClientX(moveEvent.clientX)
759
+ });
760
+ patchStops(latest);
761
+ };
762
+ const onMouseUp = () => {
763
+ setDraggingStopId(null);
764
+ if (removed) setActiveStopId(null);
765
+ document.removeEventListener("mousemove", onMouseMove);
766
+ document.removeEventListener("mouseup", onMouseUp);
767
+ };
768
+ document.addEventListener("mousemove", onMouseMove);
769
+ document.addEventListener("mouseup", onMouseUp);
770
+ };
771
+ const handleMarkerKeyDown = (stop) => (event) => {
772
+ if (disabled) return;
773
+ const step = event.shiftKey ? 10 : 1;
774
+ let handled = true;
775
+ switch (event.key) {
776
+ case "ArrowRight":
777
+ case "ArrowUp":
778
+ patchStops(
779
+ updateStop(value.stops, stop.id, { position: stop.position + step })
780
+ );
781
+ break;
782
+ case "ArrowLeft":
783
+ case "ArrowDown":
784
+ patchStops(
785
+ updateStop(value.stops, stop.id, { position: stop.position - step })
786
+ );
787
+ break;
788
+ case "Home":
789
+ patchStops(updateStop(value.stops, stop.id, { position: 0 }));
790
+ break;
791
+ case "End":
792
+ patchStops(updateStop(value.stops, stop.id, { position: 100 }));
793
+ break;
794
+ case "Enter":
795
+ case " ":
796
+ setActiveStopId((current) => current === stop.id ? null : stop.id);
797
+ break;
798
+ case "Delete":
799
+ case "Backspace":
800
+ handleRemoveStop(stop.id);
801
+ break;
802
+ default:
803
+ handled = false;
804
+ }
805
+ if (handled) event.preventDefault();
806
+ };
807
+ const typeOptions = gradientTypes.map((type) => ({
808
+ label: typeLabels?.[type] ?? DEFAULT_TYPE_LABELS[type],
809
+ value: type
810
+ }));
811
+ const showAngle = supportsAngle(value.type);
812
+ return /* @__PURE__ */ jsxs2(
813
+ Box,
814
+ {
815
+ ref: rootRef,
816
+ testID,
817
+ "data-testid": testID,
818
+ backgroundColor: theme.colors.layer.float,
819
+ borderRadius: theme.shape.contextMenu.lg.borderRadius,
820
+ padding: 16,
821
+ gap: 12,
822
+ width: 344,
823
+ style: { boxShadow: "0px 4px 16px 0px rgba(7, 7, 8, 0.1)" },
824
+ children: [
825
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 8, alignItems: "center", children: [
826
+ /* @__PURE__ */ jsx5(Box, { flex: 1, role: "group", "aria-label": typeAriaLabel, children: /* @__PURE__ */ jsx5(
827
+ Select,
828
+ {
829
+ ...themeProps,
830
+ size: "sm",
831
+ options: typeOptions,
832
+ value: value.type,
833
+ disabled,
834
+ onChange: (next) => emit({ ...value, type: next })
835
+ }
836
+ ) }),
837
+ showAngle && /* @__PURE__ */ jsx5(Box, { width: 88, children: /* @__PURE__ */ jsx5(
838
+ Input,
839
+ {
840
+ ...themeProps,
841
+ size: "sm",
842
+ inputMode: "numeric",
843
+ disabled,
844
+ "aria-label": angleAriaLabel,
845
+ iconLeft: /* @__PURE__ */ jsx5(AngleGlyph, {}),
846
+ value: `${Math.round(value.angle)}`,
847
+ onChange: (event) => {
848
+ const parsed = parseInt(event.target.value, 10);
849
+ emit({
850
+ ...value,
851
+ angle: Number.isNaN(parsed) ? 0 : (parsed % 360 + 360) % 360
852
+ });
853
+ }
854
+ }
855
+ ) })
856
+ ] }),
857
+ /* @__PURE__ */ jsx5(
858
+ Box,
859
+ {
860
+ height: TRACK_HEIGHT,
861
+ justifyContent: "center",
862
+ paddingHorizontal: MARKER_INSET,
863
+ children: /* @__PURE__ */ jsxs2(
864
+ Box,
865
+ {
866
+ ref: trackRef,
867
+ width: "100%",
868
+ height: TRACK_HEIGHT,
869
+ position: "relative",
870
+ cursor: canAdd ? "copy" : "default",
871
+ onMouseDown: isWeb && canAdd ? handleTrackPress : void 0,
872
+ children: [
873
+ /* @__PURE__ */ jsx5(
874
+ Box,
875
+ {
876
+ testID: "gradient-track",
877
+ "data-testid": "gradient-track",
878
+ "aria-hidden": true,
879
+ position: "absolute",
880
+ width: "100%",
881
+ height: TRACK_HEIGHT,
882
+ borderRadius: 999,
883
+ style: {
884
+ top: 0,
885
+ left: 0,
886
+ backgroundImage: toPreviewCssGradient(value)
887
+ }
888
+ }
889
+ ),
890
+ stops.map((stop, index) => /* @__PURE__ */ jsx5(
891
+ GradientStopMarker,
892
+ {
893
+ stop,
894
+ index: index + 1,
895
+ disabled,
896
+ active: activeStopId === stop.id || draggingStopId === stop.id,
897
+ onDragStart: handleDragStart(stop),
898
+ onKeyDown: handleMarkerKeyDown(stop),
899
+ themeMode,
900
+ themeProductContext
901
+ },
902
+ stop.id
903
+ ))
904
+ ]
905
+ }
906
+ )
907
+ }
908
+ ),
909
+ /* @__PURE__ */ jsxs2(
910
+ Box,
911
+ {
912
+ flexDirection: "row",
913
+ alignItems: "center",
914
+ justifyContent: "space-between",
915
+ children: [
916
+ /* @__PURE__ */ jsx5(Text, { color: contentColors.primary, fontSize: 14, children: stopsLabel }),
917
+ /* @__PURE__ */ jsx5(
918
+ IconButton,
919
+ {
920
+ ...themeProps,
921
+ size: "sm",
922
+ variant: "tertiary",
923
+ tone: "mono",
924
+ disabled: !canAdd,
925
+ onPress: handleAddStopPress,
926
+ icon: /* @__PURE__ */ jsx5(PlusGlyph, {}),
927
+ "aria-label": "Add stop"
928
+ }
929
+ )
930
+ ]
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsx5(Box, { gap: 8, children: stops.map((stop, index) => /* @__PURE__ */ jsxs2(
934
+ Box,
935
+ {
936
+ flexDirection: "row",
937
+ alignItems: "center",
938
+ gap: 8,
939
+ testID: `gradient-stop-row-${stop.id}`,
940
+ "data-testid": `gradient-stop-row-${stop.id}`,
941
+ children: [
942
+ /* @__PURE__ */ jsx5(Box, { width: 72, children: /* @__PURE__ */ jsx5(
943
+ Input,
944
+ {
945
+ ...themeProps,
946
+ size: "sm",
947
+ inputMode: "numeric",
948
+ disabled,
949
+ "aria-label": `Stop ${index + 1} position percent`,
950
+ iconRight: /* @__PURE__ */ jsx5(PercentGlyph, {}),
951
+ value: `${stop.position}`,
952
+ onChange: (event) => {
953
+ const parsed = parseInt(event.target.value, 10);
954
+ if (Number.isNaN(parsed)) return;
955
+ patchStops(
956
+ updateStop(value.stops, stop.id, { position: parsed })
957
+ );
958
+ }
959
+ }
960
+ ) }),
961
+ /* @__PURE__ */ jsx5(Box, { flex: 1, children: /* @__PURE__ */ jsx5(
962
+ InputColor,
963
+ {
964
+ size: "sm",
965
+ type: "color-hex",
966
+ transparency: true,
967
+ "aria-label": `Stop ${index + 1} colour`,
968
+ testID: `gradient-stop-color-${stop.id}`,
969
+ value: toHexa(stop.color, stop.opacity),
970
+ onChange: (next) => {
971
+ const { color, opacity } = fromHexa(next);
972
+ patchStops(
973
+ updateStop(value.stops, stop.id, { color, opacity })
974
+ );
975
+ },
976
+ themeMode,
977
+ themeProductContext
978
+ }
979
+ ) }),
980
+ /* @__PURE__ */ jsx5(
981
+ IconButton,
982
+ {
983
+ ...themeProps,
984
+ size: "sm",
985
+ variant: "tertiary",
986
+ tone: "mono",
987
+ disabled: !canRemove,
988
+ onPress: () => handleRemoveStop(stop.id),
989
+ icon: /* @__PURE__ */ jsx5(MinusGlyph, {}),
990
+ "aria-label": `Remove stop ${index + 1}`
991
+ }
992
+ )
993
+ ]
994
+ },
995
+ stop.id
996
+ )) }),
997
+ activeStop && !disabled && /* @__PURE__ */ jsx5(
998
+ ColorPicker,
999
+ {
1000
+ alpha: true,
1001
+ colorFormat: "hex",
1002
+ value: toHexa(activeStop.color, activeStop.opacity),
1003
+ onChange: ({
1004
+ valueInInitialFormat
1005
+ }) => {
1006
+ const { color, opacity } = fromHexa(valueInInitialFormat);
1007
+ patchStops(
1008
+ updateStop(value.stops, activeStop.id, { color, opacity })
1009
+ );
1010
+ },
1011
+ themeMode,
1012
+ themeProductContext
1013
+ }
1014
+ )
1015
+ ]
1016
+ }
1017
+ );
1018
+ };
1019
+ GradientPicker.displayName = "GradientPicker";
1020
+ export {
1021
+ DEFAULT_GRADIENT,
1022
+ GradientPicker,
1023
+ GradientStopMarker,
1024
+ addStopAt,
1025
+ interpolateColorAt,
1026
+ removeStop,
1027
+ sortStops,
1028
+ supportsAngle,
1029
+ toCssGradient,
1030
+ toPreviewCssGradient,
1031
+ updateStop
1032
+ };
1033
+ //# sourceMappingURL=index.mjs.map