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