@wix/headless-stores 0.0.75 → 0.0.76

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.
@@ -641,6 +641,135 @@ export declare const ProductMediaGallery: React.ForwardRefExoticComponent<Produc
641
641
  * Alias for ProductMediaGallery to match the documented API
642
642
  */
643
643
  export { ProductMediaGallery as MediaGallery };
644
+ /**
645
+ * Props for Product Quantity component
646
+ */
647
+ export interface ProductQuantityProps {
648
+ /** Whether to render as a child component */
649
+ asChild?: boolean;
650
+ /** Custom render function when using asChild */
651
+ children?: AsChildChildren<{
652
+ selectedQuantity: number;
653
+ availableQuantity: number | null;
654
+ inStock: boolean;
655
+ isPreOrderEnabled: boolean;
656
+ setSelectedQuantity: (quantity: number) => void;
657
+ }>;
658
+ /** CSS classes to apply to the default element */
659
+ className?: string;
660
+ }
661
+ /**
662
+ * Props for Product Quantity sub-components
663
+ */
664
+ export interface ProductQuantitySubComponentProps {
665
+ /** CSS classes to apply to the element */
666
+ className?: string;
667
+ /** Whether to render as a child component */
668
+ asChild?: boolean;
669
+ /** Custom render function when using asChild */
670
+ children?: AsChildChildren<{
671
+ disabled: boolean;
672
+ }>;
673
+ /** Whether the component is disabled */
674
+ disabled?: boolean;
675
+ }
676
+ /**
677
+ * Product quantity selector component that integrates with the selected variant.
678
+ * Provides quantity controls with stock validation and pre-order support.
679
+ * Uses a compound component pattern with Root, Decrement, Input, Increment, and Raw sub-components.
680
+ *
681
+ * @component
682
+ * @example
683
+ * ```tsx
684
+ * // Compound component usage (recommended)
685
+ * <Product.Quantity.Root className="flex items-center gap-3">
686
+ * <div className="flex items-center border border-brand-light rounded-lg">
687
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
688
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
689
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
690
+ * </div>
691
+ * <Product.Quantity.Raw asChild>
692
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
693
+ * <div>
694
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
695
+ * <span className="text-content-muted text-sm">
696
+ * Max: {availableQuantity} Pre Order
697
+ * </span>
698
+ * )}
699
+ * {inStock && availableQuantity && availableQuantity < 10 && (
700
+ * <span className="text-content-muted text-sm">
701
+ * Only {availableQuantity} left in stock
702
+ * </span>
703
+ * )}
704
+ * </div>
705
+ * )}
706
+ * </Product.Quantity.Raw>
707
+ * </Product.Quantity.Root>
708
+ *
709
+ * // Legacy asChild usage (still supported)
710
+ * <Product.Quantity asChild>
711
+ * {({ selectedQuantity, availableQuantity, inStock, setSelectedQuantity }) => (
712
+ * <div className="flex items-center gap-3">
713
+ * <div className="flex items-center border border-brand-light rounded-lg">
714
+ * <button
715
+ * onClick={() => setSelectedQuantity(selectedQuantity - 1)}
716
+ * disabled={selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled)}
717
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
718
+ * >
719
+ * -
720
+ * </button>
721
+ * <span className="px-4 py-2 border-x border-brand-light min-w-[3rem] text-center">
722
+ * {selectedQuantity}
723
+ * </span>
724
+ * <button
725
+ * onClick={() => setSelectedQuantity(selectedQuantity + 1)}
726
+ * disabled={availableQuantity && selectedQuantity >= availableQuantity}
727
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
728
+ * >
729
+ * +
730
+ * </button>
731
+ * </div>
732
+ * </div>
733
+ * )}
734
+ * </Product.Quantity>
735
+ * ```
736
+ */
737
+ export declare const ProductQuantity: React.ForwardRefExoticComponent<ProductQuantityProps & React.RefAttributes<HTMLDivElement>>;
738
+ /**
739
+ * Product Quantity Decrement component.
740
+ * Automatically handles disabled state based on stock and pre-order settings.
741
+ * Must be used within Product.Quantity.Root.
742
+ *
743
+ * @component
744
+ * @example
745
+ * ```tsx
746
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
747
+ * ```
748
+ */
749
+ export declare const ProductQuantityDecrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
750
+ /**
751
+ * Product Quantity Input component.
752
+ * Displays the current quantity value. Must be used within Product.Quantity.Root.
753
+ *
754
+ * @component
755
+ * @example
756
+ * ```tsx
757
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
758
+ * ```
759
+ */
760
+ export declare const ProductQuantityInput: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLInputElement>>;
761
+ /**
762
+ * Product Quantity Increment component.
763
+ * Automatically handles disabled state based on stock availability.
764
+ * Must be used within Product.Quantity.Root.
765
+ *
766
+ * @component
767
+ * @example
768
+ * ```tsx
769
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
770
+ * ```
771
+ */
772
+ export declare const ProductQuantityIncrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
644
773
  /**
645
774
  * Props for Product Action components following the documented API
646
775
  */
@@ -660,6 +789,50 @@ export interface ProductActionProps {
660
789
  /** Content to display when loading */
661
790
  loadingState?: string | React.ReactNode;
662
791
  }
792
+ /**
793
+ * Props for Product Quantity Raw component
794
+ */
795
+ export interface ProductQuantityRawSubComponentProps {
796
+ /** CSS classes to apply to the element */
797
+ className?: string;
798
+ /** Whether to render as a child component */
799
+ asChild?: boolean;
800
+ /** Custom render function when using asChild */
801
+ children?: AsChildChildren<{
802
+ selectedQuantity: number;
803
+ availableQuantity: number;
804
+ inStock: boolean;
805
+ isPreOrderEnabled: boolean;
806
+ setSelectedQuantity: (quantity: number) => void;
807
+ }>;
808
+ }
809
+ /**
810
+ * Product Quantity Raw component.
811
+ * Provides access to raw quantity data for custom stock messages and advanced use cases.
812
+ * Must be used within Product.Quantity.Root.
813
+ *
814
+ * @component
815
+ * @example
816
+ * ```tsx
817
+ * <Product.Quantity.Raw asChild>
818
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
819
+ * <div>
820
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
821
+ * <span className="text-content-muted text-sm">
822
+ * Max: {availableQuantity} Pre Order
823
+ * </span>
824
+ * )}
825
+ * {inStock && availableQuantity && availableQuantity < 10 && (
826
+ * <span className="text-content-muted text-sm">
827
+ * Only {availableQuantity} left in stock
828
+ * </span>
829
+ * )}
830
+ * </div>
831
+ * )}
832
+ * </Product.Quantity.Raw>
833
+ * ```
834
+ */
835
+ export declare const ProductQuantityRaw: React.ForwardRefExoticComponent<ProductQuantityRawSubComponentProps & React.RefAttributes<HTMLElement>>;
663
836
  /**
664
837
  * Add to cart action button component following the documented API.
665
838
  * Automatically integrates with the selected variant and handles loading states.
@@ -687,3 +860,19 @@ export declare const Action: {
687
860
  /** Pre-order action button */
688
861
  readonly PreOrder: React.ForwardRefExoticComponent<ProductActionProps & React.RefAttributes<HTMLButtonElement>>;
689
862
  };
863
+ /**
864
+ * Quantity namespace containing all product quantity components
865
+ * following the compound component pattern: Product.Quantity.Root, Product.Quantity.Decrement, etc.
866
+ */
867
+ export declare const Quantity: {
868
+ /** Product quantity selector component */
869
+ readonly Root: React.ForwardRefExoticComponent<ProductQuantityProps & React.RefAttributes<HTMLDivElement>>;
870
+ /** Product quantity decrement component */
871
+ readonly Decrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
872
+ /** Product quantity input component */
873
+ readonly Input: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLInputElement>>;
874
+ /** Product quantity increment component */
875
+ readonly Increment: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
876
+ /** Product quantity raw component */
877
+ readonly Raw: React.ForwardRefExoticComponent<ProductQuantityRawSubComponentProps & React.RefAttributes<HTMLElement>>;
878
+ };
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import { Commerce } from '@wix/headless-ecom/react';
5
5
  import { AsChildSlot } from '@wix/headless-utils/react';
6
6
  import { MediaGallery } from '@wix/headless-media/react';
7
+ import { Quantity as QuantityComponent } from '@wix/headless-components/react';
7
8
  import * as CoreProduct from './core/Product.js';
8
9
  import * as ProductVariantSelector from './core/ProductVariantSelector.js';
9
10
  import * as ProductModifiers from './core/ProductModifiers.js';
@@ -54,6 +55,11 @@ var TestIds;
54
55
  TestIds["productActionAddToCart"] = "product-action-add-to-cart";
55
56
  TestIds["productActionBuyNow"] = "product-action-buy-now";
56
57
  TestIds["productActionPreOrder"] = "product-action-pre-order";
58
+ TestIds["productQuantity"] = "product-quantity";
59
+ TestIds["productQuantityDecrement"] = "product-quantity-decrement";
60
+ TestIds["productQuantityInput"] = "product-quantity-input";
61
+ TestIds["productQuantityIncrement"] = "product-quantity-increment";
62
+ TestIds["productQuantityRaw"] = "product-quantity-raw";
57
63
  })(TestIds || (TestIds = {}));
58
64
  /**
59
65
  * Root component that provides all necessary service contexts for a complete product experience.
@@ -679,6 +685,177 @@ export const ProductMediaGallery = React.forwardRef((props, ref) => {
679
685
  * Alias for ProductMediaGallery to match the documented API
680
686
  */
681
687
  export { ProductMediaGallery as MediaGallery };
688
+ /**
689
+ * Product quantity selector component that integrates with the selected variant.
690
+ * Provides quantity controls with stock validation and pre-order support.
691
+ * Uses a compound component pattern with Root, Decrement, Input, Increment, and Raw sub-components.
692
+ *
693
+ * @component
694
+ * @example
695
+ * ```tsx
696
+ * // Compound component usage (recommended)
697
+ * <Product.Quantity.Root className="flex items-center gap-3">
698
+ * <div className="flex items-center border border-brand-light rounded-lg">
699
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
700
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
701
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
702
+ * </div>
703
+ * <Product.Quantity.Raw asChild>
704
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
705
+ * <div>
706
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
707
+ * <span className="text-content-muted text-sm">
708
+ * Max: {availableQuantity} Pre Order
709
+ * </span>
710
+ * )}
711
+ * {inStock && availableQuantity && availableQuantity < 10 && (
712
+ * <span className="text-content-muted text-sm">
713
+ * Only {availableQuantity} left in stock
714
+ * </span>
715
+ * )}
716
+ * </div>
717
+ * )}
718
+ * </Product.Quantity.Raw>
719
+ * </Product.Quantity.Root>
720
+ *
721
+ * // Legacy asChild usage (still supported)
722
+ * <Product.Quantity asChild>
723
+ * {({ selectedQuantity, availableQuantity, inStock, setSelectedQuantity }) => (
724
+ * <div className="flex items-center gap-3">
725
+ * <div className="flex items-center border border-brand-light rounded-lg">
726
+ * <button
727
+ * onClick={() => setSelectedQuantity(selectedQuantity - 1)}
728
+ * disabled={selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled)}
729
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
730
+ * >
731
+ * -
732
+ * </button>
733
+ * <span className="px-4 py-2 border-x border-brand-light min-w-[3rem] text-center">
734
+ * {selectedQuantity}
735
+ * </span>
736
+ * <button
737
+ * onClick={() => setSelectedQuantity(selectedQuantity + 1)}
738
+ * disabled={availableQuantity && selectedQuantity >= availableQuantity}
739
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
740
+ * >
741
+ * +
742
+ * </button>
743
+ * </div>
744
+ * </div>
745
+ * )}
746
+ * </Product.Quantity>
747
+ * ```
748
+ */
749
+ export const ProductQuantity = React.forwardRef((props, ref) => {
750
+ const { asChild, children, className } = props;
751
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ inStock, isPreOrderEnabled, availableQuantity, selectedQuantity, setSelectedQuantity, }) => {
752
+ const renderProps = {
753
+ selectedQuantity,
754
+ availableQuantity,
755
+ inStock,
756
+ isPreOrderEnabled,
757
+ setSelectedQuantity,
758
+ };
759
+ if (asChild && children) {
760
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantity, customElement: children, customElementProps: renderProps }));
761
+ }
762
+ return (_jsx(QuantityComponent.Root, { initialValue: selectedQuantity, onValueChange: setSelectedQuantity, className: className, ref: ref, "data-testid": TestIds.productQuantity, children: React.isValidElement(children) ? children : null }));
763
+ } }));
764
+ });
765
+ /**
766
+ * Product Quantity Decrement component.
767
+ * Automatically handles disabled state based on stock and pre-order settings.
768
+ * Must be used within Product.Quantity.Root.
769
+ *
770
+ * @component
771
+ * @example
772
+ * ```tsx
773
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
774
+ * ```
775
+ */
776
+ export const ProductQuantityDecrement = React.forwardRef((props, ref) => {
777
+ const { asChild, children, className } = props;
778
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ selectedQuantity, inStock, isPreOrderEnabled }) => {
779
+ const disabled = selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled);
780
+ if (asChild && children) {
781
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantityDecrement, customElement: children, customElementProps: { disabled } }));
782
+ }
783
+ return (_jsx(QuantityComponent.Decrement, { className: className, ref: ref, "data-testid": TestIds.productQuantityDecrement, disabled: disabled }));
784
+ } }));
785
+ });
786
+ /**
787
+ * Product Quantity Input component.
788
+ * Displays the current quantity value. Must be used within Product.Quantity.Root.
789
+ *
790
+ * @component
791
+ * @example
792
+ * ```tsx
793
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
794
+ * ```
795
+ */
796
+ export const ProductQuantityInput = React.forwardRef((props, ref) => {
797
+ const { asChild, children, className, disabled = true } = props;
798
+ return (_jsx(ProductVariantSelector.Stock, { children: () => {
799
+ if (asChild && children) {
800
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, disabled: disabled, "data-testid": TestIds.productQuantityInput, customElement: children, customElementProps: {} }));
801
+ }
802
+ return (_jsx(QuantityComponent.Input, { className: className, disabled: disabled, ref: ref, "data-testid": TestIds.productQuantityInput }));
803
+ } }));
804
+ });
805
+ /**
806
+ * Product Quantity Increment component.
807
+ * Automatically handles disabled state based on stock availability.
808
+ * Must be used within Product.Quantity.Root.
809
+ *
810
+ * @component
811
+ * @example
812
+ * ```tsx
813
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
814
+ * ```
815
+ */
816
+ export const ProductQuantityIncrement = React.forwardRef((props, ref) => {
817
+ const { asChild, children, className } = props;
818
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ selectedQuantity, availableQuantity, inStock, isPreOrderEnabled, }) => {
819
+ const disabled = (!!availableQuantity && selectedQuantity >= availableQuantity) ||
820
+ (!inStock && !isPreOrderEnabled);
821
+ if (asChild && children) {
822
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantityIncrement, customElement: children, customElementProps: { disabled } }));
823
+ }
824
+ return (_jsx(QuantityComponent.Increment, { className: className, ref: ref, "data-testid": TestIds.productQuantityIncrement, disabled: disabled }));
825
+ } }));
826
+ });
827
+ /**
828
+ * Product Quantity Raw component.
829
+ * Provides access to raw quantity data for custom stock messages and advanced use cases.
830
+ * Must be used within Product.Quantity.Root.
831
+ *
832
+ * @component
833
+ * @example
834
+ * ```tsx
835
+ * <Product.Quantity.Raw asChild>
836
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
837
+ * <div>
838
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
839
+ * <span className="text-content-muted text-sm">
840
+ * Max: {availableQuantity} Pre Order
841
+ * </span>
842
+ * )}
843
+ * {inStock && availableQuantity && availableQuantity < 10 && (
844
+ * <span className="text-content-muted text-sm">
845
+ * Only {availableQuantity} left in stock
846
+ * </span>
847
+ * )}
848
+ * </div>
849
+ * )}
850
+ * </Product.Quantity.Raw>
851
+ * ```
852
+ */
853
+ export const ProductQuantityRaw = React.forwardRef((props, ref) => {
854
+ const { asChild, children, className } = props;
855
+ return (_jsx(ProductVariantSelector.Stock, { children: (renderProps) => {
856
+ return (_jsx(AsChildSlot, { ref: ref, customElement: children, asChild: asChild, className: className, "data-testid": TestIds.productQuantityRaw, customElementProps: renderProps }));
857
+ } }));
858
+ });
682
859
  /**
683
860
  * Add to cart action button component following the documented API.
684
861
  * Automatically integrates with the selected variant and handles loading states.
@@ -758,3 +935,19 @@ export const Action = {
758
935
  /** Pre-order action button */
759
936
  PreOrder: ProductActionPreOrder,
760
937
  };
938
+ /**
939
+ * Quantity namespace containing all product quantity components
940
+ * following the compound component pattern: Product.Quantity.Root, Product.Quantity.Decrement, etc.
941
+ */
942
+ export const Quantity = {
943
+ /** Product quantity selector component */
944
+ Root: ProductQuantity,
945
+ /** Product quantity decrement component */
946
+ Decrement: ProductQuantityDecrement,
947
+ /** Product quantity input component */
948
+ Input: ProductQuantityInput,
949
+ /** Product quantity increment component */
950
+ Increment: ProductQuantityIncrement,
951
+ /** Product quantity raw component */
952
+ Raw: ProductQuantityRaw,
953
+ };
@@ -236,6 +236,8 @@ export interface StockRenderProps {
236
236
  incrementQuantity: () => void;
237
237
  /** Function to decrement quantity */
238
238
  decrementQuantity: () => void;
239
+ /** Function to set selected quantity */
240
+ setSelectedQuantity: (quantity: number) => void;
239
241
  }
240
242
  /**
241
243
  * Headless component for product stock status
@@ -238,6 +238,9 @@ export function Stock(props) {
238
238
  const decrementQuantity = () => {
239
239
  variantService.decrementQuantity();
240
240
  };
241
+ const setSelectedQuantity = (quantity) => {
242
+ variantService.setSelectedQuantity(quantity);
243
+ };
241
244
  return props.children({
242
245
  inStock,
243
246
  availableQuantity,
@@ -247,6 +250,7 @@ export function Stock(props) {
247
250
  trackInventory,
248
251
  selectedQuantity,
249
252
  incrementQuantity,
253
+ setSelectedQuantity,
250
254
  decrementQuantity,
251
255
  });
252
256
  }
@@ -641,6 +641,135 @@ export declare const ProductMediaGallery: React.ForwardRefExoticComponent<Produc
641
641
  * Alias for ProductMediaGallery to match the documented API
642
642
  */
643
643
  export { ProductMediaGallery as MediaGallery };
644
+ /**
645
+ * Props for Product Quantity component
646
+ */
647
+ export interface ProductQuantityProps {
648
+ /** Whether to render as a child component */
649
+ asChild?: boolean;
650
+ /** Custom render function when using asChild */
651
+ children?: AsChildChildren<{
652
+ selectedQuantity: number;
653
+ availableQuantity: number | null;
654
+ inStock: boolean;
655
+ isPreOrderEnabled: boolean;
656
+ setSelectedQuantity: (quantity: number) => void;
657
+ }>;
658
+ /** CSS classes to apply to the default element */
659
+ className?: string;
660
+ }
661
+ /**
662
+ * Props for Product Quantity sub-components
663
+ */
664
+ export interface ProductQuantitySubComponentProps {
665
+ /** CSS classes to apply to the element */
666
+ className?: string;
667
+ /** Whether to render as a child component */
668
+ asChild?: boolean;
669
+ /** Custom render function when using asChild */
670
+ children?: AsChildChildren<{
671
+ disabled: boolean;
672
+ }>;
673
+ /** Whether the component is disabled */
674
+ disabled?: boolean;
675
+ }
676
+ /**
677
+ * Product quantity selector component that integrates with the selected variant.
678
+ * Provides quantity controls with stock validation and pre-order support.
679
+ * Uses a compound component pattern with Root, Decrement, Input, Increment, and Raw sub-components.
680
+ *
681
+ * @component
682
+ * @example
683
+ * ```tsx
684
+ * // Compound component usage (recommended)
685
+ * <Product.Quantity.Root className="flex items-center gap-3">
686
+ * <div className="flex items-center border border-brand-light rounded-lg">
687
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
688
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
689
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
690
+ * </div>
691
+ * <Product.Quantity.Raw asChild>
692
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
693
+ * <div>
694
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
695
+ * <span className="text-content-muted text-sm">
696
+ * Max: {availableQuantity} Pre Order
697
+ * </span>
698
+ * )}
699
+ * {inStock && availableQuantity && availableQuantity < 10 && (
700
+ * <span className="text-content-muted text-sm">
701
+ * Only {availableQuantity} left in stock
702
+ * </span>
703
+ * )}
704
+ * </div>
705
+ * )}
706
+ * </Product.Quantity.Raw>
707
+ * </Product.Quantity.Root>
708
+ *
709
+ * // Legacy asChild usage (still supported)
710
+ * <Product.Quantity asChild>
711
+ * {({ selectedQuantity, availableQuantity, inStock, setSelectedQuantity }) => (
712
+ * <div className="flex items-center gap-3">
713
+ * <div className="flex items-center border border-brand-light rounded-lg">
714
+ * <button
715
+ * onClick={() => setSelectedQuantity(selectedQuantity - 1)}
716
+ * disabled={selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled)}
717
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
718
+ * >
719
+ * -
720
+ * </button>
721
+ * <span className="px-4 py-2 border-x border-brand-light min-w-[3rem] text-center">
722
+ * {selectedQuantity}
723
+ * </span>
724
+ * <button
725
+ * onClick={() => setSelectedQuantity(selectedQuantity + 1)}
726
+ * disabled={availableQuantity && selectedQuantity >= availableQuantity}
727
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
728
+ * >
729
+ * +
730
+ * </button>
731
+ * </div>
732
+ * </div>
733
+ * )}
734
+ * </Product.Quantity>
735
+ * ```
736
+ */
737
+ export declare const ProductQuantity: React.ForwardRefExoticComponent<ProductQuantityProps & React.RefAttributes<HTMLDivElement>>;
738
+ /**
739
+ * Product Quantity Decrement component.
740
+ * Automatically handles disabled state based on stock and pre-order settings.
741
+ * Must be used within Product.Quantity.Root.
742
+ *
743
+ * @component
744
+ * @example
745
+ * ```tsx
746
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
747
+ * ```
748
+ */
749
+ export declare const ProductQuantityDecrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
750
+ /**
751
+ * Product Quantity Input component.
752
+ * Displays the current quantity value. Must be used within Product.Quantity.Root.
753
+ *
754
+ * @component
755
+ * @example
756
+ * ```tsx
757
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
758
+ * ```
759
+ */
760
+ export declare const ProductQuantityInput: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLInputElement>>;
761
+ /**
762
+ * Product Quantity Increment component.
763
+ * Automatically handles disabled state based on stock availability.
764
+ * Must be used within Product.Quantity.Root.
765
+ *
766
+ * @component
767
+ * @example
768
+ * ```tsx
769
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
770
+ * ```
771
+ */
772
+ export declare const ProductQuantityIncrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
644
773
  /**
645
774
  * Props for Product Action components following the documented API
646
775
  */
@@ -660,6 +789,50 @@ export interface ProductActionProps {
660
789
  /** Content to display when loading */
661
790
  loadingState?: string | React.ReactNode;
662
791
  }
792
+ /**
793
+ * Props for Product Quantity Raw component
794
+ */
795
+ export interface ProductQuantityRawSubComponentProps {
796
+ /** CSS classes to apply to the element */
797
+ className?: string;
798
+ /** Whether to render as a child component */
799
+ asChild?: boolean;
800
+ /** Custom render function when using asChild */
801
+ children?: AsChildChildren<{
802
+ selectedQuantity: number;
803
+ availableQuantity: number;
804
+ inStock: boolean;
805
+ isPreOrderEnabled: boolean;
806
+ setSelectedQuantity: (quantity: number) => void;
807
+ }>;
808
+ }
809
+ /**
810
+ * Product Quantity Raw component.
811
+ * Provides access to raw quantity data for custom stock messages and advanced use cases.
812
+ * Must be used within Product.Quantity.Root.
813
+ *
814
+ * @component
815
+ * @example
816
+ * ```tsx
817
+ * <Product.Quantity.Raw asChild>
818
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
819
+ * <div>
820
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
821
+ * <span className="text-content-muted text-sm">
822
+ * Max: {availableQuantity} Pre Order
823
+ * </span>
824
+ * )}
825
+ * {inStock && availableQuantity && availableQuantity < 10 && (
826
+ * <span className="text-content-muted text-sm">
827
+ * Only {availableQuantity} left in stock
828
+ * </span>
829
+ * )}
830
+ * </div>
831
+ * )}
832
+ * </Product.Quantity.Raw>
833
+ * ```
834
+ */
835
+ export declare const ProductQuantityRaw: React.ForwardRefExoticComponent<ProductQuantityRawSubComponentProps & React.RefAttributes<HTMLElement>>;
663
836
  /**
664
837
  * Add to cart action button component following the documented API.
665
838
  * Automatically integrates with the selected variant and handles loading states.
@@ -687,3 +860,19 @@ export declare const Action: {
687
860
  /** Pre-order action button */
688
861
  readonly PreOrder: React.ForwardRefExoticComponent<ProductActionProps & React.RefAttributes<HTMLButtonElement>>;
689
862
  };
863
+ /**
864
+ * Quantity namespace containing all product quantity components
865
+ * following the compound component pattern: Product.Quantity.Root, Product.Quantity.Decrement, etc.
866
+ */
867
+ export declare const Quantity: {
868
+ /** Product quantity selector component */
869
+ readonly Root: React.ForwardRefExoticComponent<ProductQuantityProps & React.RefAttributes<HTMLDivElement>>;
870
+ /** Product quantity decrement component */
871
+ readonly Decrement: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
872
+ /** Product quantity input component */
873
+ readonly Input: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLInputElement>>;
874
+ /** Product quantity increment component */
875
+ readonly Increment: React.ForwardRefExoticComponent<ProductQuantitySubComponentProps & React.RefAttributes<HTMLButtonElement>>;
876
+ /** Product quantity raw component */
877
+ readonly Raw: React.ForwardRefExoticComponent<ProductQuantityRawSubComponentProps & React.RefAttributes<HTMLElement>>;
878
+ };
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import { Commerce } from '@wix/headless-ecom/react';
5
5
  import { AsChildSlot } from '@wix/headless-utils/react';
6
6
  import { MediaGallery } from '@wix/headless-media/react';
7
+ import { Quantity as QuantityComponent } from '@wix/headless-components/react';
7
8
  import * as CoreProduct from './core/Product.js';
8
9
  import * as ProductVariantSelector from './core/ProductVariantSelector.js';
9
10
  import * as ProductModifiers from './core/ProductModifiers.js';
@@ -54,6 +55,11 @@ var TestIds;
54
55
  TestIds["productActionAddToCart"] = "product-action-add-to-cart";
55
56
  TestIds["productActionBuyNow"] = "product-action-buy-now";
56
57
  TestIds["productActionPreOrder"] = "product-action-pre-order";
58
+ TestIds["productQuantity"] = "product-quantity";
59
+ TestIds["productQuantityDecrement"] = "product-quantity-decrement";
60
+ TestIds["productQuantityInput"] = "product-quantity-input";
61
+ TestIds["productQuantityIncrement"] = "product-quantity-increment";
62
+ TestIds["productQuantityRaw"] = "product-quantity-raw";
57
63
  })(TestIds || (TestIds = {}));
58
64
  /**
59
65
  * Root component that provides all necessary service contexts for a complete product experience.
@@ -679,6 +685,177 @@ export const ProductMediaGallery = React.forwardRef((props, ref) => {
679
685
  * Alias for ProductMediaGallery to match the documented API
680
686
  */
681
687
  export { ProductMediaGallery as MediaGallery };
688
+ /**
689
+ * Product quantity selector component that integrates with the selected variant.
690
+ * Provides quantity controls with stock validation and pre-order support.
691
+ * Uses a compound component pattern with Root, Decrement, Input, Increment, and Raw sub-components.
692
+ *
693
+ * @component
694
+ * @example
695
+ * ```tsx
696
+ * // Compound component usage (recommended)
697
+ * <Product.Quantity.Root className="flex items-center gap-3">
698
+ * <div className="flex items-center border border-brand-light rounded-lg">
699
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
700
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
701
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
702
+ * </div>
703
+ * <Product.Quantity.Raw asChild>
704
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
705
+ * <div>
706
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
707
+ * <span className="text-content-muted text-sm">
708
+ * Max: {availableQuantity} Pre Order
709
+ * </span>
710
+ * )}
711
+ * {inStock && availableQuantity && availableQuantity < 10 && (
712
+ * <span className="text-content-muted text-sm">
713
+ * Only {availableQuantity} left in stock
714
+ * </span>
715
+ * )}
716
+ * </div>
717
+ * )}
718
+ * </Product.Quantity.Raw>
719
+ * </Product.Quantity.Root>
720
+ *
721
+ * // Legacy asChild usage (still supported)
722
+ * <Product.Quantity asChild>
723
+ * {({ selectedQuantity, availableQuantity, inStock, setSelectedQuantity }) => (
724
+ * <div className="flex items-center gap-3">
725
+ * <div className="flex items-center border border-brand-light rounded-lg">
726
+ * <button
727
+ * onClick={() => setSelectedQuantity(selectedQuantity - 1)}
728
+ * disabled={selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled)}
729
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
730
+ * >
731
+ * -
732
+ * </button>
733
+ * <span className="px-4 py-2 border-x border-brand-light min-w-[3rem] text-center">
734
+ * {selectedQuantity}
735
+ * </span>
736
+ * <button
737
+ * onClick={() => setSelectedQuantity(selectedQuantity + 1)}
738
+ * disabled={availableQuantity && selectedQuantity >= availableQuantity}
739
+ * className="px-3 py-2 hover:bg-surface-primary disabled:opacity-50"
740
+ * >
741
+ * +
742
+ * </button>
743
+ * </div>
744
+ * </div>
745
+ * )}
746
+ * </Product.Quantity>
747
+ * ```
748
+ */
749
+ export const ProductQuantity = React.forwardRef((props, ref) => {
750
+ const { asChild, children, className } = props;
751
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ inStock, isPreOrderEnabled, availableQuantity, selectedQuantity, setSelectedQuantity, }) => {
752
+ const renderProps = {
753
+ selectedQuantity,
754
+ availableQuantity,
755
+ inStock,
756
+ isPreOrderEnabled,
757
+ setSelectedQuantity,
758
+ };
759
+ if (asChild && children) {
760
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantity, customElement: children, customElementProps: renderProps }));
761
+ }
762
+ return (_jsx(QuantityComponent.Root, { initialValue: selectedQuantity, onValueChange: setSelectedQuantity, className: className, ref: ref, "data-testid": TestIds.productQuantity, children: React.isValidElement(children) ? children : null }));
763
+ } }));
764
+ });
765
+ /**
766
+ * Product Quantity Decrement component.
767
+ * Automatically handles disabled state based on stock and pre-order settings.
768
+ * Must be used within Product.Quantity.Root.
769
+ *
770
+ * @component
771
+ * @example
772
+ * ```tsx
773
+ * <Product.Quantity.Decrement className="px-3 py-1 hover:bg-surface-primary transition-colors" />
774
+ * ```
775
+ */
776
+ export const ProductQuantityDecrement = React.forwardRef((props, ref) => {
777
+ const { asChild, children, className } = props;
778
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ selectedQuantity, inStock, isPreOrderEnabled }) => {
779
+ const disabled = selectedQuantity <= 1 || (!inStock && !isPreOrderEnabled);
780
+ if (asChild && children) {
781
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantityDecrement, customElement: children, customElementProps: { disabled } }));
782
+ }
783
+ return (_jsx(QuantityComponent.Decrement, { className: className, ref: ref, "data-testid": TestIds.productQuantityDecrement, disabled: disabled }));
784
+ } }));
785
+ });
786
+ /**
787
+ * Product Quantity Input component.
788
+ * Displays the current quantity value. Must be used within Product.Quantity.Root.
789
+ *
790
+ * @component
791
+ * @example
792
+ * ```tsx
793
+ * <Product.Quantity.Input className="w-16 text-center py-1 border-x border-brand-light focus:outline-none focus:ring-2 focus:ring-brand-primary" />
794
+ * ```
795
+ */
796
+ export const ProductQuantityInput = React.forwardRef((props, ref) => {
797
+ const { asChild, children, className, disabled = true } = props;
798
+ return (_jsx(ProductVariantSelector.Stock, { children: () => {
799
+ if (asChild && children) {
800
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, disabled: disabled, "data-testid": TestIds.productQuantityInput, customElement: children, customElementProps: {} }));
801
+ }
802
+ return (_jsx(QuantityComponent.Input, { className: className, disabled: disabled, ref: ref, "data-testid": TestIds.productQuantityInput }));
803
+ } }));
804
+ });
805
+ /**
806
+ * Product Quantity Increment component.
807
+ * Automatically handles disabled state based on stock availability.
808
+ * Must be used within Product.Quantity.Root.
809
+ *
810
+ * @component
811
+ * @example
812
+ * ```tsx
813
+ * <Product.Quantity.Increment className="px-3 py-1 hover:bg-surface-primary transition-colors" />
814
+ * ```
815
+ */
816
+ export const ProductQuantityIncrement = React.forwardRef((props, ref) => {
817
+ const { asChild, children, className } = props;
818
+ return (_jsx(ProductVariantSelector.Stock, { children: ({ selectedQuantity, availableQuantity, inStock, isPreOrderEnabled, }) => {
819
+ const disabled = (!!availableQuantity && selectedQuantity >= availableQuantity) ||
820
+ (!inStock && !isPreOrderEnabled);
821
+ if (asChild && children) {
822
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productQuantityIncrement, customElement: children, customElementProps: { disabled } }));
823
+ }
824
+ return (_jsx(QuantityComponent.Increment, { className: className, ref: ref, "data-testid": TestIds.productQuantityIncrement, disabled: disabled }));
825
+ } }));
826
+ });
827
+ /**
828
+ * Product Quantity Raw component.
829
+ * Provides access to raw quantity data for custom stock messages and advanced use cases.
830
+ * Must be used within Product.Quantity.Root.
831
+ *
832
+ * @component
833
+ * @example
834
+ * ```tsx
835
+ * <Product.Quantity.Raw asChild>
836
+ * {({ availableQuantity, inStock, isPreOrderEnabled }) => (
837
+ * <div>
838
+ * {!inStock && isPreOrderEnabled && availableQuantity && (
839
+ * <span className="text-content-muted text-sm">
840
+ * Max: {availableQuantity} Pre Order
841
+ * </span>
842
+ * )}
843
+ * {inStock && availableQuantity && availableQuantity < 10 && (
844
+ * <span className="text-content-muted text-sm">
845
+ * Only {availableQuantity} left in stock
846
+ * </span>
847
+ * )}
848
+ * </div>
849
+ * )}
850
+ * </Product.Quantity.Raw>
851
+ * ```
852
+ */
853
+ export const ProductQuantityRaw = React.forwardRef((props, ref) => {
854
+ const { asChild, children, className } = props;
855
+ return (_jsx(ProductVariantSelector.Stock, { children: (renderProps) => {
856
+ return (_jsx(AsChildSlot, { ref: ref, customElement: children, asChild: asChild, className: className, "data-testid": TestIds.productQuantityRaw, customElementProps: renderProps }));
857
+ } }));
858
+ });
682
859
  /**
683
860
  * Add to cart action button component following the documented API.
684
861
  * Automatically integrates with the selected variant and handles loading states.
@@ -758,3 +935,19 @@ export const Action = {
758
935
  /** Pre-order action button */
759
936
  PreOrder: ProductActionPreOrder,
760
937
  };
938
+ /**
939
+ * Quantity namespace containing all product quantity components
940
+ * following the compound component pattern: Product.Quantity.Root, Product.Quantity.Decrement, etc.
941
+ */
942
+ export const Quantity = {
943
+ /** Product quantity selector component */
944
+ Root: ProductQuantity,
945
+ /** Product quantity decrement component */
946
+ Decrement: ProductQuantityDecrement,
947
+ /** Product quantity input component */
948
+ Input: ProductQuantityInput,
949
+ /** Product quantity increment component */
950
+ Increment: ProductQuantityIncrement,
951
+ /** Product quantity raw component */
952
+ Raw: ProductQuantityRaw,
953
+ };
@@ -236,6 +236,8 @@ export interface StockRenderProps {
236
236
  incrementQuantity: () => void;
237
237
  /** Function to decrement quantity */
238
238
  decrementQuantity: () => void;
239
+ /** Function to set selected quantity */
240
+ setSelectedQuantity: (quantity: number) => void;
239
241
  }
240
242
  /**
241
243
  * Headless component for product stock status
@@ -238,6 +238,9 @@ export function Stock(props) {
238
238
  const decrementQuantity = () => {
239
239
  variantService.decrementQuantity();
240
240
  };
241
+ const setSelectedQuantity = (quantity) => {
242
+ variantService.setSelectedQuantity(quantity);
243
+ };
241
244
  return props.children({
242
245
  inStock,
243
246
  availableQuantity,
@@ -247,6 +250,7 @@ export function Stock(props) {
247
250
  trackInventory,
248
251
  selectedQuantity,
249
252
  incrementQuantity,
253
+ setSelectedQuantity,
250
254
  decrementQuantity,
251
255
  });
252
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-stores",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "prebuild": "cd ../media && yarn build && cd ../ecom && yarn build",
@@ -62,7 +62,7 @@
62
62
  "@wix/auto_sdk_stores_read-only-variants-v-3": "^1.0.23",
63
63
  "@wix/ecom": "^1.0.1278",
64
64
  "@wix/essentials": "^0.1.24",
65
- "@wix/headless-components": "0.0.11",
65
+ "@wix/headless-components": "0.0.12",
66
66
  "@wix/headless-ecom": "0.0.24",
67
67
  "@wix/headless-media": "0.0.11",
68
68
  "@wix/headless-utils": "0.0.3",