@xsolla/xui-button 0.79.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/index.js CHANGED
@@ -38,7 +38,7 @@ __export(index_exports, {
38
38
  module.exports = __toCommonJS(index_exports);
39
39
 
40
40
  // src/Button.tsx
41
- var import_react4 = require("react");
41
+ var import_react4 = __toESM(require("react"));
42
42
 
43
43
  // ../primitives-web/src/Box.tsx
44
44
  var import_react = __toESM(require("react"));
@@ -497,6 +497,17 @@ TextAreaPrimitive.displayName = "TextAreaPrimitive";
497
497
  // src/Button.tsx
498
498
  var import_xui_core = require("@xsolla/xui-core");
499
499
  var import_jsx_runtime8 = require("react/jsx-runtime");
500
+ var cloneIconWithDefaults = (icon, defaultSize, defaultColor) => {
501
+ if (!import_react4.default.isValidElement(icon)) return icon;
502
+ const iconElement = icon;
503
+ const existingProps = iconElement.props || {};
504
+ return import_react4.default.cloneElement(iconElement, {
505
+ ...existingProps,
506
+ // Preserve existing props (including accessibility attributes)
507
+ size: existingProps.size ?? defaultSize,
508
+ color: existingProps.color ?? defaultColor
509
+ });
510
+ };
500
511
  var Button = ({
501
512
  variant = "primary",
502
513
  tone = "brand",
@@ -507,6 +518,11 @@ var Button = ({
507
518
  onPress,
508
519
  iconLeft,
509
520
  iconRight,
521
+ divider,
522
+ sublabel,
523
+ labelAlignment = "center",
524
+ labelIcon,
525
+ customContent,
510
526
  "aria-label": ariaLabel,
511
527
  "aria-describedby": ariaDescribedBy,
512
528
  "aria-expanded": ariaExpanded,
@@ -522,9 +538,17 @@ var Button = ({
522
538
  const [isKeyboardPressed, setIsKeyboardPressed] = (0, import_react4.useState)(false);
523
539
  const isDisabled = disabled || loading;
524
540
  const sizeStyles = theme.sizing.button(size);
525
- const variantStyles = theme?.colors?.control?.[tone]?.[variant] || theme?.colors?.control?.brand?.primary || {
541
+ const controlTone = theme?.colors?.control?.[tone];
542
+ const variantStyles = controlTone?.[variant] || theme?.colors?.control?.brand?.primary || {
526
543
  bg: "transparent",
527
- text: { primary: "#000" }
544
+ bgHover: "transparent",
545
+ bgPress: "transparent",
546
+ bgDisable: "transparent",
547
+ border: "transparent",
548
+ borderHover: "transparent",
549
+ borderPress: "transparent",
550
+ borderDisable: "transparent",
551
+ text: { primary: "#000", secondary: "#000", disable: "#666" }
528
552
  };
529
553
  const handlePress = () => {
530
554
  if (!isDisabled && onPress) {
@@ -548,17 +572,19 @@ var Button = ({
548
572
  }
549
573
  }
550
574
  };
551
- const styles = variantStyles;
552
- let backgroundColor = styles.bg;
575
+ let backgroundColor = variantStyles.bg;
553
576
  if (disabled) {
554
- backgroundColor = styles.bgDisable || styles.bg;
577
+ backgroundColor = variantStyles.bgDisable || variantStyles.bg;
555
578
  } else if (isKeyboardPressed) {
556
- backgroundColor = styles.bgPress || styles.bg;
579
+ backgroundColor = variantStyles.bgPress || variantStyles.bg;
557
580
  }
558
- const borderColor = disabled ? styles.borderDisable || styles.border : styles.border;
559
- const textColor = disabled ? styles.text?.disable || styles.text?.primary : styles.text?.primary;
560
- const isDarkText = textColor === "#000000" || textColor === "black" || textColor.startsWith("rgba(0, 0, 0");
561
- const dividerColor = isDarkText ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)";
581
+ const borderColor = disabled ? variantStyles.borderDisable || variantStyles.border : variantStyles.border;
582
+ const textColor = disabled ? variantStyles.text?.disable || variantStyles.text?.primary : variantStyles.text?.primary;
583
+ const textColorStr = typeof textColor === "string" ? textColor : "";
584
+ const isDarkText = textColorStr === "#000000" || textColorStr === "black" || textColorStr.startsWith("rgba(0, 0, 0");
585
+ const dividerLineColor = isDarkText ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)";
586
+ const hasIcon = Boolean(iconLeft || iconRight);
587
+ const showDivider = divider !== void 0 ? divider : hasIcon;
562
588
  const computedAriaLabel = ariaLabel;
563
589
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
564
590
  Box,
@@ -582,7 +608,7 @@ var Button = ({
582
608
  backgroundColor,
583
609
  borderColor,
584
610
  borderWidth: borderColor !== "transparent" && borderColor !== "rgba(255, 255, 255, 0)" ? 1 : 0,
585
- borderRadius: theme.radius.button,
611
+ borderRadius: sizeStyles.borderRadius,
586
612
  height: sizeStyles.height,
587
613
  width: fullWidth ? "100%" : void 0,
588
614
  padding: 0,
@@ -605,72 +631,110 @@ var Button = ({
605
631
  outlineStyle: "solid"
606
632
  },
607
633
  children: [
608
- !loading && iconLeft && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
634
+ loading && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
635
+ Box,
636
+ {
637
+ position: "absolute",
638
+ top: 0,
639
+ left: 0,
640
+ right: 0,
641
+ bottom: 0,
642
+ alignItems: "center",
643
+ justifyContent: "center",
644
+ zIndex: 1,
645
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
646
+ Spinner,
647
+ {
648
+ color: textColor,
649
+ size: sizeStyles.spinnerSize,
650
+ "aria-hidden": true
651
+ }
652
+ )
653
+ }
654
+ ),
655
+ iconLeft && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
609
656
  Box,
610
657
  {
611
658
  height: "100%",
612
659
  flexDirection: "row",
613
660
  alignItems: "center",
614
- justifyContent: "center",
615
661
  "aria-hidden": true,
662
+ style: {
663
+ opacity: loading ? 0 : 1,
664
+ pointerEvents: loading ? "none" : "auto"
665
+ },
616
666
  children: [
617
667
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
618
668
  Box,
619
669
  {
670
+ width: sizeStyles.iconContainerSize,
671
+ height: sizeStyles.iconContainerSize,
620
672
  alignItems: "center",
621
673
  justifyContent: "center",
622
- paddingHorizontal: sizeStyles.iconPadding,
623
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, children: iconLeft })
674
+ children: cloneIconWithDefaults(iconLeft, sizeStyles.iconSize, textColor)
624
675
  }
625
676
  ),
626
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerColor, height: "100%" })
677
+ showDivider && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerLineColor, height: "100%" })
627
678
  ]
628
679
  }
629
680
  ),
630
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
681
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
631
682
  Box,
632
683
  {
633
684
  flex: fullWidth ? 1 : void 0,
634
685
  flexDirection: "row",
635
686
  alignItems: "center",
636
- justifyContent: "center",
637
- paddingHorizontal: loading ? sizeStyles.loadingPadding : sizeStyles.padding,
687
+ justifyContent: labelAlignment === "left" ? "flex-start" : "center",
688
+ paddingHorizontal: sizeStyles.padding,
638
689
  height: "100%",
639
- children: loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
640
- Spinner,
641
- {
642
- color: textColor,
643
- size: sizeStyles.spinnerSize,
644
- "aria-hidden": true
645
- }
646
- ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
647
- Text,
648
- {
649
- color: textColor,
650
- fontSize: sizeStyles.fontSize,
651
- fontWeight: "500",
652
- children
653
- }
654
- )
690
+ gap: sizeStyles.labelIconGap,
691
+ style: {
692
+ opacity: loading ? 0 : 1,
693
+ pointerEvents: loading ? "none" : "auto"
694
+ },
695
+ "aria-hidden": loading ? true : void 0,
696
+ children: [
697
+ labelIcon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { "aria-hidden": true, children: cloneIconWithDefaults(
698
+ labelIcon,
699
+ sizeStyles.labelIconSize,
700
+ textColor
701
+ ) }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Text, { color: textColor, fontSize: sizeStyles.fontSize, fontWeight: "500", children }),
703
+ sublabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
704
+ Text,
705
+ {
706
+ color: textColor,
707
+ fontSize: sizeStyles.fontSize,
708
+ fontWeight: "500",
709
+ style: { opacity: 0.4 },
710
+ children: sublabel
711
+ }
712
+ ),
713
+ customContent && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { "aria-hidden": true, children: customContent })
714
+ ]
655
715
  }
656
716
  ),
657
- !loading && iconRight && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
717
+ iconRight && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
658
718
  Box,
659
719
  {
660
720
  height: "100%",
661
721
  flexDirection: "row",
662
722
  alignItems: "center",
663
- justifyContent: "center",
664
723
  "aria-hidden": true,
724
+ style: {
725
+ opacity: loading ? 0 : 1,
726
+ pointerEvents: loading ? "none" : "auto"
727
+ },
665
728
  children: [
666
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerColor, height: "100%" }),
729
+ showDivider && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerLineColor, height: "100%" }),
667
730
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
668
731
  Box,
669
732
  {
733
+ width: sizeStyles.iconContainerSize,
734
+ height: sizeStyles.iconContainerSize,
670
735
  alignItems: "center",
671
736
  justifyContent: "center",
672
- paddingHorizontal: sizeStyles.iconPadding,
673
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, children: iconRight })
737
+ children: cloneIconWithDefaults(iconRight, sizeStyles.iconSize, textColor)
674
738
  }
675
739
  )
676
740
  ]
@@ -683,9 +747,20 @@ var Button = ({
683
747
  Button.displayName = "Button";
684
748
 
685
749
  // src/IconButton.tsx
686
- var import_react5 = require("react");
750
+ var import_react5 = __toESM(require("react"));
687
751
  var import_xui_core2 = require("@xsolla/xui-core");
688
752
  var import_jsx_runtime9 = require("react/jsx-runtime");
753
+ var cloneIconWithDefaults2 = (icon, defaultSize, defaultColor) => {
754
+ if (!import_react5.default.isValidElement(icon)) return icon;
755
+ const iconElement = icon;
756
+ const existingProps = iconElement.props || {};
757
+ return import_react5.default.cloneElement(iconElement, {
758
+ ...existingProps,
759
+ // Preserve existing props (including accessibility attributes)
760
+ size: existingProps.size ?? defaultSize,
761
+ color: existingProps.color ?? defaultColor
762
+ });
763
+ };
689
764
  var IconButton = ({
690
765
  variant = "primary",
691
766
  tone = "brand",
@@ -708,9 +783,17 @@ var IconButton = ({
708
783
  const [isKeyboardPressed, setIsKeyboardPressed] = (0, import_react5.useState)(false);
709
784
  const isDisabled = disabled || loading;
710
785
  const sizeStyles = theme.sizing.button(size);
711
- const variantStyles = theme?.colors?.control?.[tone]?.[variant] || theme?.colors?.control?.brand?.primary || {
786
+ const controlTone = theme?.colors?.control?.[tone];
787
+ const variantStyles = controlTone?.[variant] || theme?.colors?.control?.brand?.primary || {
712
788
  bg: "transparent",
713
- text: { primary: "#000" }
789
+ bgHover: "transparent",
790
+ bgPress: "transparent",
791
+ bgDisable: "transparent",
792
+ border: "transparent",
793
+ borderHover: "transparent",
794
+ borderPress: "transparent",
795
+ borderDisable: "transparent",
796
+ text: { primary: "#000", secondary: "#000", disable: "#666" }
714
797
  };
715
798
  const handlePress = () => {
716
799
  if (!isDisabled && onPress) {
@@ -734,16 +817,15 @@ var IconButton = ({
734
817
  }
735
818
  }
736
819
  };
737
- const styles = variantStyles;
738
- let backgroundColor = styles.bg;
820
+ let backgroundColor = variantStyles.bg;
739
821
  if (disabled) {
740
- backgroundColor = styles.bgDisable || styles.bg;
822
+ backgroundColor = variantStyles.bgDisable || variantStyles.bg;
741
823
  } else if (isKeyboardPressed) {
742
- backgroundColor = styles.bgPress || styles.bg;
824
+ backgroundColor = variantStyles.bgPress || variantStyles.bg;
743
825
  }
744
- const borderColor = disabled ? styles.borderDisable || styles.border : styles.border;
745
- const textColor = disabled ? styles.text?.disable || styles.text?.primary : styles.text?.primary;
746
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
826
+ const borderColor = disabled ? variantStyles.borderDisable || variantStyles.border : variantStyles.border;
827
+ const textColor = disabled ? variantStyles.text?.disable || variantStyles.text?.primary : variantStyles.text?.primary;
828
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
747
829
  Box,
748
830
  {
749
831
  as: "button",
@@ -765,7 +847,7 @@ var IconButton = ({
765
847
  backgroundColor,
766
848
  borderColor,
767
849
  borderWidth: borderColor !== "transparent" && borderColor !== "rgba(255, 255, 255, 0)" ? 1 : 0,
768
- borderRadius: theme.radius.button,
850
+ borderRadius: sizeStyles.borderRadius,
769
851
  height: sizeStyles.height,
770
852
  width: sizeStyles.height,
771
853
  padding: 0,
@@ -776,10 +858,10 @@ var IconButton = ({
776
858
  cursor: disabled ? "not-allowed" : loading ? "wait" : "pointer",
777
859
  opacity: disabled ? 0.6 : 1,
778
860
  hoverStyle: !isDisabled ? {
779
- backgroundColor: styles.bgHover
861
+ backgroundColor: variantStyles.bgHover
780
862
  } : void 0,
781
863
  pressStyle: !isDisabled ? {
782
- backgroundColor: styles.bgPress
864
+ backgroundColor: variantStyles.bgPress
783
865
  } : void 0,
784
866
  focusStyle: {
785
867
  outlineColor: theme.colors.border.brand,
@@ -787,14 +869,40 @@ var IconButton = ({
787
869
  outlineOffset: 2,
788
870
  outlineStyle: "solid"
789
871
  },
790
- children: loading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
791
- Spinner,
792
- {
793
- color: textColor,
794
- size: sizeStyles.spinnerSize,
795
- "aria-hidden": true
796
- }
797
- ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, "aria-hidden": true, children: icon })
872
+ children: [
873
+ loading && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
874
+ Box,
875
+ {
876
+ position: "absolute",
877
+ top: 0,
878
+ left: 0,
879
+ right: 0,
880
+ bottom: 0,
881
+ alignItems: "center",
882
+ justifyContent: "center",
883
+ zIndex: 1,
884
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
885
+ Spinner,
886
+ {
887
+ color: textColor,
888
+ size: sizeStyles.spinnerSize,
889
+ "aria-hidden": true
890
+ }
891
+ )
892
+ }
893
+ ),
894
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
895
+ Box,
896
+ {
897
+ "aria-hidden": true,
898
+ style: {
899
+ opacity: loading ? 0 : 1,
900
+ pointerEvents: loading ? "none" : "auto"
901
+ },
902
+ children: cloneIconWithDefaults2(icon, sizeStyles.iconSize, textColor)
903
+ }
904
+ )
905
+ ]
798
906
  }
799
907
  );
800
908
  };
@@ -1187,7 +1295,7 @@ var ButtonGroup = ({
1187
1295
  const computedAriaDescribedBy = [
1188
1296
  ariaDescribedBy,
1189
1297
  error && errorId ? errorId : void 0,
1190
- description && !error && descriptionId ? descriptionId : void 0
1298
+ description && descriptionId ? descriptionId : void 0
1191
1299
  ].filter(Boolean).join(" ") || void 0;
1192
1300
  const processChildren = (childrenToProcess) => {
1193
1301
  if (orientation === "vertical") {
@@ -1250,7 +1358,7 @@ var ButtonGroup = ({
1250
1358
  children: error
1251
1359
  }
1252
1360
  ) }),
1253
- description && !error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Box, { marginTop: 4, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1361
+ description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Box, { marginTop: 4, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1254
1362
  Text,
1255
1363
  {
1256
1364
  id: descriptionId,
package/web/index.js.flow CHANGED
@@ -10,7 +10,7 @@ declare interface ButtonProps {
10
10
  /**
11
11
  * Visual variant of the button
12
12
  */
13
- variant?: "primary" | "secondary";
13
+ variant?: "primary" | "secondary" | "tertiary";
14
14
 
15
15
  /**
16
16
  * Color tone of the button
@@ -43,15 +43,46 @@ declare interface ButtonProps {
43
43
  onPress?: () => void;
44
44
 
45
45
  /**
46
- * Icon to display on the left side
46
+ * Icon to display on the left side.
47
+ * Size and color are automatically set based on button size/state.
48
+ * To override, specify size/color on the icon: `iconLeft={<ArrowLeft size={16} />}`
47
49
  */
48
50
  iconLeft?: React.ReactNode;
49
51
 
50
52
  /**
51
- * Icon to display on the right side
53
+ * Icon to display on the right side.
54
+ * Size and color are automatically set based on button size/state.
55
+ * To override, specify size/color on the icon: `iconRight={<ArrowRight size={16} />}`
52
56
  */
53
57
  iconRight?: React.ReactNode;
54
58
 
59
+ /**
60
+ * Show/hide vertical divider between icon and content (default: true when icon present)
61
+ */
62
+ divider?: boolean;
63
+
64
+ /**
65
+ * Secondary text displayed inline with the main label (e.g., price), shown with 40% opacity
66
+ */
67
+ sublabel?: string;
68
+
69
+ /**
70
+ * Alignment of the label text
71
+ */
72
+ labelAlignment?: "left" | "center";
73
+
74
+ /**
75
+ * Small icon displayed directly next to the label text.
76
+ * Size and color are automatically set based on button size/state.
77
+ * To override, specify size/color on the icon: `labelIcon={<InfoIcon size={12} />}`
78
+ */
79
+ labelIcon?: React.ReactNode;
80
+
81
+ /**
82
+ * Custom content slot for badges, tags, or other elements
83
+ */
84
+ customContent?: React.ReactNode;
85
+
55
86
  /**
56
87
  * Accessible label for screen readers (use for icon-only buttons)
57
88
  */
@@ -98,7 +129,7 @@ declare interface ButtonProps {
98
129
  type?: "button" | "submit" | "reset";
99
130
 
100
131
  /**
101
- * Whether the button should take up the full width of its container
132
+ * Whether the button should stretch to fill the full width of its container
102
133
  */
103
134
  fullWidth?: boolean;
104
135
  }
@@ -121,7 +152,7 @@ declare interface IconButtonProps {
121
152
  /**
122
153
  * Visual variant of the button
123
154
  */
124
- variant?: "primary" | "secondary";
155
+ variant?: "primary" | "secondary" | "tertiary";
125
156
 
126
157
  /**
127
158
  * Color tone of the button
@@ -144,7 +175,9 @@ declare interface IconButtonProps {
144
175
  loading?: boolean;
145
176
 
146
177
  /**
147
- * Icon to display in the button (required)
178
+ * Icon to display in the button (required).
179
+ * Size and color are automatically set based on button size/state.
180
+ * To override, specify size/color on the icon: `icon={<CloseIcon size={16} />}`
148
181
  */
149
182
  icon: React.ReactNode;
150
183