@xsolla/xui-button 0.78.0 → 0.79.0-pr122.1769779574

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.
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
 
3
3
  interface ButtonProps {
4
4
  /** Visual variant of the button */
5
- variant?: "primary" | "secondary";
5
+ variant?: "primary" | "secondary" | "tertiary";
6
6
  /** Color tone of the button */
7
7
  tone?: "brand" | "brandExtra" | "alert" | "mono";
8
8
  /** Size of the button */
@@ -15,10 +15,32 @@ interface ButtonProps {
15
15
  children: React.ReactNode;
16
16
  /** Click handler */
17
17
  onPress?: () => void;
18
- /** Icon to display on the left side */
18
+ /**
19
+ * Icon to display on the left side.
20
+ * Size and color are automatically set based on button size/state.
21
+ * To override, specify size/color on the icon: `iconLeft={<ArrowLeft size={16} />}`
22
+ */
19
23
  iconLeft?: React.ReactNode;
20
- /** Icon to display on the right side */
24
+ /**
25
+ * Icon to display on the right side.
26
+ * Size and color are automatically set based on button size/state.
27
+ * To override, specify size/color on the icon: `iconRight={<ArrowRight size={16} />}`
28
+ */
21
29
  iconRight?: React.ReactNode;
30
+ /** Show/hide vertical divider between icon and content (default: true when icon present) */
31
+ divider?: boolean;
32
+ /** Secondary text displayed inline with the main label (e.g., price), shown with 40% opacity */
33
+ sublabel?: string;
34
+ /** Alignment of the label text */
35
+ labelAlignment?: "left" | "center";
36
+ /**
37
+ * Small icon displayed directly next to the label text.
38
+ * Size and color are automatically set based on button size/state.
39
+ * To override, specify size/color on the icon: `labelIcon={<InfoIcon size={12} />}`
40
+ */
41
+ labelIcon?: React.ReactNode;
42
+ /** Custom content slot for badges, tags, or other elements */
43
+ customContent?: React.ReactNode;
22
44
  /** Accessible label for screen readers (use for icon-only buttons) */
23
45
  "aria-label"?: string;
24
46
  /** ID of element that describes this button */
@@ -37,7 +59,7 @@ interface ButtonProps {
37
59
  id?: string;
38
60
  /** HTML type attribute for the button */
39
61
  type?: "button" | "submit" | "reset";
40
- /** Whether the button should take up the full width of its container */
62
+ /** Whether the button should stretch to fill the full width of its container */
41
63
  fullWidth?: boolean;
42
64
  }
43
65
  /**
@@ -59,7 +81,7 @@ declare const Button: React.FC<ButtonProps>;
59
81
 
60
82
  interface IconButtonProps {
61
83
  /** Visual variant of the button */
62
- variant?: "primary" | "secondary";
84
+ variant?: "primary" | "secondary" | "tertiary";
63
85
  /** Color tone of the button */
64
86
  tone?: "brand" | "brandExtra" | "alert" | "mono";
65
87
  /** Size of the button */
@@ -68,7 +90,11 @@ interface IconButtonProps {
68
90
  disabled?: boolean;
69
91
  /** Whether the button is in a loading state */
70
92
  loading?: boolean;
71
- /** Icon to display in the button (required) */
93
+ /**
94
+ * Icon to display in the button (required).
95
+ * Size and color are automatically set based on button size/state.
96
+ * To override, specify size/color on the icon: `icon={<CloseIcon size={16} />}`
97
+ */
72
98
  icon: React.ReactNode;
73
99
  /** Click handler */
74
100
  onPress?: () => void;
package/native/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
 
3
3
  interface ButtonProps {
4
4
  /** Visual variant of the button */
5
- variant?: "primary" | "secondary";
5
+ variant?: "primary" | "secondary" | "tertiary";
6
6
  /** Color tone of the button */
7
7
  tone?: "brand" | "brandExtra" | "alert" | "mono";
8
8
  /** Size of the button */
@@ -15,10 +15,32 @@ interface ButtonProps {
15
15
  children: React.ReactNode;
16
16
  /** Click handler */
17
17
  onPress?: () => void;
18
- /** Icon to display on the left side */
18
+ /**
19
+ * Icon to display on the left side.
20
+ * Size and color are automatically set based on button size/state.
21
+ * To override, specify size/color on the icon: `iconLeft={<ArrowLeft size={16} />}`
22
+ */
19
23
  iconLeft?: React.ReactNode;
20
- /** Icon to display on the right side */
24
+ /**
25
+ * Icon to display on the right side.
26
+ * Size and color are automatically set based on button size/state.
27
+ * To override, specify size/color on the icon: `iconRight={<ArrowRight size={16} />}`
28
+ */
21
29
  iconRight?: React.ReactNode;
30
+ /** Show/hide vertical divider between icon and content (default: true when icon present) */
31
+ divider?: boolean;
32
+ /** Secondary text displayed inline with the main label (e.g., price), shown with 40% opacity */
33
+ sublabel?: string;
34
+ /** Alignment of the label text */
35
+ labelAlignment?: "left" | "center";
36
+ /**
37
+ * Small icon displayed directly next to the label text.
38
+ * Size and color are automatically set based on button size/state.
39
+ * To override, specify size/color on the icon: `labelIcon={<InfoIcon size={12} />}`
40
+ */
41
+ labelIcon?: React.ReactNode;
42
+ /** Custom content slot for badges, tags, or other elements */
43
+ customContent?: React.ReactNode;
22
44
  /** Accessible label for screen readers (use for icon-only buttons) */
23
45
  "aria-label"?: string;
24
46
  /** ID of element that describes this button */
@@ -37,7 +59,7 @@ interface ButtonProps {
37
59
  id?: string;
38
60
  /** HTML type attribute for the button */
39
61
  type?: "button" | "submit" | "reset";
40
- /** Whether the button should take up the full width of its container */
62
+ /** Whether the button should stretch to fill the full width of its container */
41
63
  fullWidth?: boolean;
42
64
  }
43
65
  /**
@@ -59,7 +81,7 @@ declare const Button: React.FC<ButtonProps>;
59
81
 
60
82
  interface IconButtonProps {
61
83
  /** Visual variant of the button */
62
- variant?: "primary" | "secondary";
84
+ variant?: "primary" | "secondary" | "tertiary";
63
85
  /** Color tone of the button */
64
86
  tone?: "brand" | "brandExtra" | "alert" | "mono";
65
87
  /** Size of the button */
@@ -68,7 +90,11 @@ interface IconButtonProps {
68
90
  disabled?: boolean;
69
91
  /** Whether the button is in a loading state */
70
92
  loading?: boolean;
71
- /** Icon to display in the button (required) */
93
+ /**
94
+ * Icon to display in the button (required).
95
+ * Size and color are automatically set based on button size/state.
96
+ * To override, specify size/color on the icon: `icon={<CloseIcon size={16} />}`
97
+ */
72
98
  icon: React.ReactNode;
73
99
  /** Click handler */
74
100
  onPress?: () => void;
package/native/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-native/src/Box.tsx
44
44
  var import_react_native = require("react-native");
@@ -250,7 +250,6 @@ var Spinner = ({
250
250
  role,
251
251
  "aria-label": ariaLabel,
252
252
  "aria-live": ariaLive,
253
- "aria-describedby": ariaDescribedBy,
254
253
  testID
255
254
  }) => {
256
255
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -369,14 +368,11 @@ var InputPrimitive = (0, import_react2.forwardRef)(
369
368
  fontSize,
370
369
  placeholderTextColor,
371
370
  maxLength,
372
- name,
373
371
  type,
374
372
  inputMode,
375
373
  autoComplete,
376
374
  id,
377
- "aria-invalid": ariaInvalid,
378
375
  "aria-describedby": ariaDescribedBy,
379
- "aria-labelledby": ariaLabelledBy,
380
376
  "aria-label": ariaLabel,
381
377
  "aria-disabled": ariaDisabled,
382
378
  "data-testid": dataTestId
@@ -468,9 +464,7 @@ var TextAreaPrimitive = (0, import_react3.forwardRef)(
468
464
  maxLength,
469
465
  rows,
470
466
  id,
471
- "aria-invalid": ariaInvalid,
472
467
  "aria-describedby": ariaDescribedBy,
473
- "aria-labelledby": ariaLabelledBy,
474
468
  "aria-label": ariaLabel,
475
469
  "aria-disabled": ariaDisabled,
476
470
  "data-testid": dataTestId
@@ -542,6 +536,17 @@ TextAreaPrimitive.displayName = "TextAreaPrimitive";
542
536
  // src/Button.tsx
543
537
  var import_xui_core = require("@xsolla/xui-core");
544
538
  var import_jsx_runtime8 = require("react/jsx-runtime");
539
+ var cloneIconWithDefaults = (icon, defaultSize, defaultColor) => {
540
+ if (!import_react4.default.isValidElement(icon)) return icon;
541
+ const iconElement = icon;
542
+ const existingProps = iconElement.props || {};
543
+ return import_react4.default.cloneElement(iconElement, {
544
+ ...existingProps,
545
+ // Preserve existing props (including accessibility attributes)
546
+ size: existingProps.size ?? defaultSize,
547
+ color: existingProps.color ?? defaultColor
548
+ });
549
+ };
545
550
  var Button = ({
546
551
  variant = "primary",
547
552
  tone = "brand",
@@ -552,6 +557,11 @@ var Button = ({
552
557
  onPress,
553
558
  iconLeft,
554
559
  iconRight,
560
+ divider,
561
+ sublabel,
562
+ labelAlignment = "center",
563
+ labelIcon,
564
+ customContent,
555
565
  "aria-label": ariaLabel,
556
566
  "aria-describedby": ariaDescribedBy,
557
567
  "aria-expanded": ariaExpanded,
@@ -567,9 +577,17 @@ var Button = ({
567
577
  const [isKeyboardPressed, setIsKeyboardPressed] = (0, import_react4.useState)(false);
568
578
  const isDisabled = disabled || loading;
569
579
  const sizeStyles = theme.sizing.button(size);
570
- const variantStyles = theme?.colors?.control?.[tone]?.[variant] || theme?.colors?.control?.brand?.primary || {
580
+ const controlTone = theme?.colors?.control?.[tone];
581
+ const variantStyles = controlTone?.[variant] || theme?.colors?.control?.brand?.primary || {
571
582
  bg: "transparent",
572
- text: { primary: "#000" }
583
+ bgHover: "transparent",
584
+ bgPress: "transparent",
585
+ bgDisable: "transparent",
586
+ border: "transparent",
587
+ borderHover: "transparent",
588
+ borderPress: "transparent",
589
+ borderDisable: "transparent",
590
+ text: { primary: "#000", secondary: "#000", disable: "#666" }
573
591
  };
574
592
  const handlePress = () => {
575
593
  if (!isDisabled && onPress) {
@@ -593,17 +611,19 @@ var Button = ({
593
611
  }
594
612
  }
595
613
  };
596
- const styles = variantStyles;
597
- let backgroundColor = styles.bg;
614
+ let backgroundColor = variantStyles.bg;
598
615
  if (disabled) {
599
- backgroundColor = styles.bgDisable || styles.bg;
616
+ backgroundColor = variantStyles.bgDisable || variantStyles.bg;
600
617
  } else if (isKeyboardPressed) {
601
- backgroundColor = styles.bgPress || styles.bg;
618
+ backgroundColor = variantStyles.bgPress || variantStyles.bg;
602
619
  }
603
- const borderColor = disabled ? styles.borderDisable || styles.border : styles.border;
604
- const textColor = disabled ? styles.text?.disable || styles.text?.primary : styles.text?.primary;
605
- const isDarkText = textColor === "#000000" || textColor === "black" || textColor.startsWith("rgba(0, 0, 0");
606
- const dividerColor = isDarkText ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)";
620
+ const borderColor = disabled ? variantStyles.borderDisable || variantStyles.border : variantStyles.border;
621
+ const textColor = disabled ? variantStyles.text?.disable || variantStyles.text?.primary : variantStyles.text?.primary;
622
+ const textColorStr = typeof textColor === "string" ? textColor : "";
623
+ const isDarkText = textColorStr === "#000000" || textColorStr === "black" || textColorStr.startsWith("rgba(0, 0, 0");
624
+ const dividerLineColor = isDarkText ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)";
625
+ const hasIcon = Boolean(iconLeft || iconRight);
626
+ const showDivider = divider !== void 0 ? divider : hasIcon;
607
627
  const computedAriaLabel = ariaLabel;
608
628
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
609
629
  Box,
@@ -627,7 +647,7 @@ var Button = ({
627
647
  backgroundColor,
628
648
  borderColor,
629
649
  borderWidth: borderColor !== "transparent" && borderColor !== "rgba(255, 255, 255, 0)" ? 1 : 0,
630
- borderRadius: theme.radius.button,
650
+ borderRadius: sizeStyles.borderRadius,
631
651
  height: sizeStyles.height,
632
652
  width: fullWidth ? "100%" : void 0,
633
653
  padding: 0,
@@ -650,72 +670,110 @@ var Button = ({
650
670
  outlineStyle: "solid"
651
671
  },
652
672
  children: [
653
- !loading && iconLeft && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
673
+ loading && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
674
+ Box,
675
+ {
676
+ position: "absolute",
677
+ top: 0,
678
+ left: 0,
679
+ right: 0,
680
+ bottom: 0,
681
+ alignItems: "center",
682
+ justifyContent: "center",
683
+ zIndex: 1,
684
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
685
+ Spinner,
686
+ {
687
+ color: textColor,
688
+ size: sizeStyles.spinnerSize,
689
+ "aria-hidden": true
690
+ }
691
+ )
692
+ }
693
+ ),
694
+ iconLeft && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
654
695
  Box,
655
696
  {
656
697
  height: "100%",
657
698
  flexDirection: "row",
658
699
  alignItems: "center",
659
- justifyContent: "center",
660
700
  "aria-hidden": true,
701
+ style: {
702
+ opacity: loading ? 0 : 1,
703
+ pointerEvents: loading ? "none" : "auto"
704
+ },
661
705
  children: [
662
706
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
663
707
  Box,
664
708
  {
709
+ width: sizeStyles.iconContainerSize,
710
+ height: sizeStyles.iconContainerSize,
665
711
  alignItems: "center",
666
712
  justifyContent: "center",
667
- paddingHorizontal: sizeStyles.iconPadding,
668
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, children: iconLeft })
713
+ children: cloneIconWithDefaults(iconLeft, sizeStyles.iconSize, textColor)
669
714
  }
670
715
  ),
671
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerColor, height: "100%" })
716
+ showDivider && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerLineColor, height: "100%" })
672
717
  ]
673
718
  }
674
719
  ),
675
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
720
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
676
721
  Box,
677
722
  {
678
723
  flex: fullWidth ? 1 : void 0,
679
724
  flexDirection: "row",
680
725
  alignItems: "center",
681
- justifyContent: "center",
682
- paddingHorizontal: loading ? sizeStyles.loadingPadding : sizeStyles.padding,
726
+ justifyContent: labelAlignment === "left" ? "flex-start" : "center",
727
+ paddingHorizontal: sizeStyles.padding,
683
728
  height: "100%",
684
- children: loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
685
- Spinner,
686
- {
687
- color: textColor,
688
- size: sizeStyles.spinnerSize,
689
- "aria-hidden": true
690
- }
691
- ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
692
- Text,
693
- {
694
- color: textColor,
695
- fontSize: sizeStyles.fontSize,
696
- fontWeight: "500",
697
- children
698
- }
699
- )
729
+ gap: sizeStyles.labelIconGap,
730
+ style: {
731
+ opacity: loading ? 0 : 1,
732
+ pointerEvents: loading ? "none" : "auto"
733
+ },
734
+ "aria-hidden": loading ? true : void 0,
735
+ children: [
736
+ labelIcon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { "aria-hidden": true, children: cloneIconWithDefaults(
737
+ labelIcon,
738
+ sizeStyles.labelIconSize,
739
+ textColor
740
+ ) }),
741
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Text, { color: textColor, fontSize: sizeStyles.fontSize, fontWeight: "500", children }),
742
+ sublabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
743
+ Text,
744
+ {
745
+ color: textColor,
746
+ fontSize: sizeStyles.fontSize,
747
+ fontWeight: "500",
748
+ style: { opacity: 0.4 },
749
+ children: sublabel
750
+ }
751
+ ),
752
+ customContent && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { "aria-hidden": true, children: customContent })
753
+ ]
700
754
  }
701
755
  ),
702
- !loading && iconRight && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
756
+ iconRight && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
703
757
  Box,
704
758
  {
705
759
  height: "100%",
706
760
  flexDirection: "row",
707
761
  alignItems: "center",
708
- justifyContent: "center",
709
762
  "aria-hidden": true,
763
+ style: {
764
+ opacity: loading ? 0 : 1,
765
+ pointerEvents: loading ? "none" : "auto"
766
+ },
710
767
  children: [
711
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerColor, height: "100%" }),
768
+ showDivider && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Divider, { vertical: true, color: dividerLineColor, height: "100%" }),
712
769
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
713
770
  Box,
714
771
  {
772
+ width: sizeStyles.iconContainerSize,
773
+ height: sizeStyles.iconContainerSize,
715
774
  alignItems: "center",
716
775
  justifyContent: "center",
717
- paddingHorizontal: sizeStyles.iconPadding,
718
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, children: iconRight })
776
+ children: cloneIconWithDefaults(iconRight, sizeStyles.iconSize, textColor)
719
777
  }
720
778
  )
721
779
  ]
@@ -728,9 +786,20 @@ var Button = ({
728
786
  Button.displayName = "Button";
729
787
 
730
788
  // src/IconButton.tsx
731
- var import_react5 = require("react");
789
+ var import_react5 = __toESM(require("react"));
732
790
  var import_xui_core2 = require("@xsolla/xui-core");
733
791
  var import_jsx_runtime9 = require("react/jsx-runtime");
792
+ var cloneIconWithDefaults2 = (icon, defaultSize, defaultColor) => {
793
+ if (!import_react5.default.isValidElement(icon)) return icon;
794
+ const iconElement = icon;
795
+ const existingProps = iconElement.props || {};
796
+ return import_react5.default.cloneElement(iconElement, {
797
+ ...existingProps,
798
+ // Preserve existing props (including accessibility attributes)
799
+ size: existingProps.size ?? defaultSize,
800
+ color: existingProps.color ?? defaultColor
801
+ });
802
+ };
734
803
  var IconButton = ({
735
804
  variant = "primary",
736
805
  tone = "brand",
@@ -753,9 +822,17 @@ var IconButton = ({
753
822
  const [isKeyboardPressed, setIsKeyboardPressed] = (0, import_react5.useState)(false);
754
823
  const isDisabled = disabled || loading;
755
824
  const sizeStyles = theme.sizing.button(size);
756
- const variantStyles = theme?.colors?.control?.[tone]?.[variant] || theme?.colors?.control?.brand?.primary || {
825
+ const controlTone = theme?.colors?.control?.[tone];
826
+ const variantStyles = controlTone?.[variant] || theme?.colors?.control?.brand?.primary || {
757
827
  bg: "transparent",
758
- text: { primary: "#000" }
828
+ bgHover: "transparent",
829
+ bgPress: "transparent",
830
+ bgDisable: "transparent",
831
+ border: "transparent",
832
+ borderHover: "transparent",
833
+ borderPress: "transparent",
834
+ borderDisable: "transparent",
835
+ text: { primary: "#000", secondary: "#000", disable: "#666" }
759
836
  };
760
837
  const handlePress = () => {
761
838
  if (!isDisabled && onPress) {
@@ -779,16 +856,15 @@ var IconButton = ({
779
856
  }
780
857
  }
781
858
  };
782
- const styles = variantStyles;
783
- let backgroundColor = styles.bg;
859
+ let backgroundColor = variantStyles.bg;
784
860
  if (disabled) {
785
- backgroundColor = styles.bgDisable || styles.bg;
861
+ backgroundColor = variantStyles.bgDisable || variantStyles.bg;
786
862
  } else if (isKeyboardPressed) {
787
- backgroundColor = styles.bgPress || styles.bg;
863
+ backgroundColor = variantStyles.bgPress || variantStyles.bg;
788
864
  }
789
- const borderColor = disabled ? styles.borderDisable || styles.border : styles.border;
790
- const textColor = disabled ? styles.text?.disable || styles.text?.primary : styles.text?.primary;
791
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
865
+ const borderColor = disabled ? variantStyles.borderDisable || variantStyles.border : variantStyles.border;
866
+ const textColor = disabled ? variantStyles.text?.disable || variantStyles.text?.primary : variantStyles.text?.primary;
867
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
792
868
  Box,
793
869
  {
794
870
  as: "button",
@@ -810,7 +886,7 @@ var IconButton = ({
810
886
  backgroundColor,
811
887
  borderColor,
812
888
  borderWidth: borderColor !== "transparent" && borderColor !== "rgba(255, 255, 255, 0)" ? 1 : 0,
813
- borderRadius: theme.radius.button,
889
+ borderRadius: sizeStyles.borderRadius,
814
890
  height: sizeStyles.height,
815
891
  width: sizeStyles.height,
816
892
  padding: 0,
@@ -821,10 +897,10 @@ var IconButton = ({
821
897
  cursor: disabled ? "not-allowed" : loading ? "wait" : "pointer",
822
898
  opacity: disabled ? 0.6 : 1,
823
899
  hoverStyle: !isDisabled ? {
824
- backgroundColor: styles.bgHover
900
+ backgroundColor: variantStyles.bgHover
825
901
  } : void 0,
826
902
  pressStyle: !isDisabled ? {
827
- backgroundColor: styles.bgPress
903
+ backgroundColor: variantStyles.bgPress
828
904
  } : void 0,
829
905
  focusStyle: {
830
906
  outlineColor: theme.colors.border.brand,
@@ -832,14 +908,40 @@ var IconButton = ({
832
908
  outlineOffset: 2,
833
909
  outlineStyle: "solid"
834
910
  },
835
- children: loading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
836
- Spinner,
837
- {
838
- color: textColor,
839
- size: sizeStyles.spinnerSize,
840
- "aria-hidden": true
841
- }
842
- ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, "aria-hidden": true, children: icon })
911
+ children: [
912
+ loading && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
913
+ Box,
914
+ {
915
+ position: "absolute",
916
+ top: 0,
917
+ left: 0,
918
+ right: 0,
919
+ bottom: 0,
920
+ alignItems: "center",
921
+ justifyContent: "center",
922
+ zIndex: 1,
923
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
924
+ Spinner,
925
+ {
926
+ color: textColor,
927
+ size: sizeStyles.spinnerSize,
928
+ "aria-hidden": true
929
+ }
930
+ )
931
+ }
932
+ ),
933
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
934
+ Box,
935
+ {
936
+ "aria-hidden": true,
937
+ style: {
938
+ opacity: loading ? 0 : 1,
939
+ pointerEvents: loading ? "none" : "auto"
940
+ },
941
+ children: cloneIconWithDefaults2(icon, sizeStyles.iconSize, textColor)
942
+ }
943
+ )
944
+ ]
843
945
  }
844
946
  );
845
947
  };
@@ -1232,7 +1334,7 @@ var ButtonGroup = ({
1232
1334
  const computedAriaDescribedBy = [
1233
1335
  ariaDescribedBy,
1234
1336
  error && errorId ? errorId : void 0,
1235
- description && !error && descriptionId ? descriptionId : void 0
1337
+ description && descriptionId ? descriptionId : void 0
1236
1338
  ].filter(Boolean).join(" ") || void 0;
1237
1339
  const processChildren = (childrenToProcess) => {
1238
1340
  if (orientation === "vertical") {
@@ -1295,7 +1397,7 @@ var ButtonGroup = ({
1295
1397
  children: error
1296
1398
  }
1297
1399
  ) }),
1298
- description && !error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Box, { marginTop: 4, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1400
+ description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Box, { marginTop: 4, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1299
1401
  Text,
1300
1402
  {
1301
1403
  id: descriptionId,
@@ -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