@propeller-commerce/propeller-v2-react-ui 0.26.1 → 0.27.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/dist/index.d.cts CHANGED
@@ -1548,6 +1548,8 @@ interface QuickOrderMatch {
1548
1548
  grossPrice: number;
1549
1549
  /** Minimum order quantity (defaults to 1). */
1550
1550
  minQuantity: number;
1551
+ /** Order step: quantities run min, min+step, min+2*step, … (defaults to 1). */
1552
+ step: number;
1551
1553
  /** Thumbnail URL, if any. */
1552
1554
  imageUrl: string;
1553
1555
  /** Price is quoted on request — not orderable through a bulk flow. */
package/dist/index.d.ts CHANGED
@@ -1548,6 +1548,8 @@ interface QuickOrderMatch {
1548
1548
  grossPrice: number;
1549
1549
  /** Minimum order quantity (defaults to 1). */
1550
1550
  minQuantity: number;
1551
+ /** Order step: quantities run min, min+step, min+2*step, … (defaults to 1). */
1552
+ step: number;
1551
1553
  /** Thumbnail URL, if any. */
1552
1554
  imageUrl: string;
1553
1555
  /** Price is quoted on request — not orderable through a bulk flow. */
package/dist/index.js CHANGED
@@ -2614,6 +2614,7 @@ function toMatch(item, language) {
2614
2614
  const netPrice = displayItem?.price?.net ?? 0;
2615
2615
  const grossPrice = displayItem?.price?.gross ?? 0;
2616
2616
  const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2617
+ const step = Math.max(1, displayItem?.unit ?? 1);
2617
2618
  const imageUrl = isCluster ? getClusterImageUrl(item) : getProductImageUrl(item);
2618
2619
  return {
2619
2620
  productId,
@@ -2623,6 +2624,7 @@ function toMatch(item, language) {
2623
2624
  netPrice,
2624
2625
  grossPrice,
2625
2626
  minQuantity,
2627
+ step,
2626
2628
  imageUrl,
2627
2629
  priceOnRequest: isPriceOnRequest(item)
2628
2630
  };
@@ -4759,6 +4761,7 @@ function AddToCart(rawProps) {
4759
4761
  onCartCreated: props.onCartCreated
4760
4762
  });
4761
4763
  const [quantity, setQuantity] = useState(() => getMinQuantity(props.product));
4764
+ const [quantityDraft, setQuantityDraft] = useState(null);
4762
4765
  const [toastMessage, setToastMessage] = useState(() => "");
4763
4766
  const [toastType, setToastType] = useState(() => "");
4764
4767
  const [toastVisible, setToastVisible] = useState(() => false);
@@ -4781,16 +4784,42 @@ function AddToCart(rawProps) {
4781
4784
  function dismissToast() {
4782
4785
  setToastVisible(false);
4783
4786
  }
4787
+ function snapQuantity(value) {
4788
+ const min = getMinQuantity(props.product);
4789
+ const step = getStep(props.product);
4790
+ if (value <= min) return min;
4791
+ return Math.round((value - min) / step) * step + min;
4792
+ }
4784
4793
  function increment() {
4785
- setQuantity(quantity + getStep(props.product));
4794
+ setQuantityDraft(null);
4795
+ const snapped = snapQuantity(quantity);
4796
+ setQuantity(snapped === quantity ? quantity + getStep(props.product) : snapped);
4786
4797
  }
4787
4798
  function decrement() {
4788
4799
  const min = getMinQuantity(props.product);
4789
4800
  const step = getStep(props.product);
4801
+ setQuantityDraft(null);
4802
+ const snapped = snapQuantity(quantity);
4803
+ if (snapped !== quantity) {
4804
+ setQuantity(snapped);
4805
+ return;
4806
+ }
4790
4807
  if (quantity - step >= min) {
4791
4808
  setQuantity(quantity - step);
4792
4809
  }
4793
4810
  }
4811
+ function handleQuantityInput(raw) {
4812
+ setQuantityDraft(raw);
4813
+ const val = parseInt(raw, 10);
4814
+ if (!isNaN(val) && val >= getMinQuantity(props.product)) {
4815
+ setQuantity(snapQuantity(val));
4816
+ }
4817
+ }
4818
+ function handleQuantityBlur() {
4819
+ const val = parseInt(quantityDraft ?? "", 10);
4820
+ setQuantity(isNaN(val) ? getMinQuantity(props.product) : snapQuantity(val));
4821
+ setQuantityDraft(null);
4822
+ }
4794
4823
  function getModalImageUrl() {
4795
4824
  if (addedCartItem) {
4796
4825
  const img = addedCartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url;
@@ -4920,15 +4949,9 @@ function AddToCart(rawProps) {
4920
4949
  className: "propeller-add-to-cart__quantity flex-1 md:flex-none md:w-12 text-center text-sm bg-transparent border-none focus:ring-0 focus:outline-none h-full [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
4921
4950
  min: getMinQuantity(props.product),
4922
4951
  step: getStep(props.product),
4923
- value: quantity,
4924
- onChange: (e) => {
4925
- const val = parseInt(e.target.value, 10);
4926
- const min = getMinQuantity(props.product);
4927
- const step = getStep(props.product);
4928
- if (!isNaN(val) && val >= min) {
4929
- setQuantity(Math.round((val - min) / step) * step + min);
4930
- }
4931
- }
4952
+ value: quantityDraft ?? quantity,
4953
+ onChange: (e) => handleQuantityInput(e.target.value),
4954
+ onBlur: handleQuantityBlur
4932
4955
  }
4933
4956
  ),
4934
4957
  /* @__PURE__ */ jsxs(
@@ -4953,15 +4976,9 @@ function AddToCart(rawProps) {
4953
4976
  className: "propeller-add-to-cart__quantity w-full md:w-16 h-10 text-center text-sm border border-input rounded-control focus:ring-2 focus:ring-secondary focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
4954
4977
  min: getMinQuantity(props.product),
4955
4978
  step: getStep(props.product),
4956
- value: quantity,
4957
- onChange: (e) => {
4958
- const val = parseInt(e.target.value, 10);
4959
- const min = getMinQuantity(props.product);
4960
- const step = getStep(props.product);
4961
- if (!isNaN(val) && val >= min) {
4962
- setQuantity(Math.round((val - min) / step) * step + min);
4963
- }
4964
- }
4979
+ value: quantityDraft ?? quantity,
4980
+ onChange: (e) => handleQuantityInput(e.target.value),
4981
+ onBlur: handleQuantityBlur
4965
4982
  }
4966
4983
  ) : null,
4967
4984
  /* @__PURE__ */ jsxs(
@@ -13883,6 +13900,28 @@ function OrderActions(rawProps) {
13883
13900
  );
13884
13901
  }
13885
13902
  var OrderActions_default = OrderActions;
13903
+ var warnedOnce = /* @__PURE__ */ new Set();
13904
+ function warnOnce(key, message) {
13905
+ if (warnedOnce.has(key)) return;
13906
+ warnedOnce.add(key);
13907
+ console.warn(message);
13908
+ }
13909
+ function warnMissingProps(props) {
13910
+ if (process.env.NODE_ENV === "production") return;
13911
+ if (!props.language) {
13912
+ warnOnce(
13913
+ "language",
13914
+ "[OrderItemCard] No `language` prop: product names resolve in the default catalogue language, whatever page they are on. This component reads no context (it ships from the RSC-safe /pure entry), so the host must pass it."
13915
+ );
13916
+ }
13917
+ const linkable = props.titleLinkable !== void 0 ? props.titleLinkable : true;
13918
+ if (linkable && !props.configuration?.urls) {
13919
+ warnOnce(
13920
+ "urls",
13921
+ "[OrderItemCard] Title is linkable but no `configuration.urls` was passed: links fall back to a literal /product/:id/:slug and lose the locale prefix. Pass the url builders from the host, or set titleLinkable={false}."
13922
+ );
13923
+ }
13924
+ }
13886
13925
  function getProductName3(orderItem, language) {
13887
13926
  return getLocalizedValue(orderItem?.product?.names, language, "") || orderItem?.name || "Unknown Product";
13888
13927
  }
@@ -13906,6 +13945,7 @@ function getProductUrl(orderItem, language, urls) {
13906
13945
  function OrderItemCard(props) {
13907
13946
  const item = props.orderItem;
13908
13947
  const isChildItem = props.isChildItem || false;
13948
+ warnMissingProps(props);
13909
13949
  const titleLinkable = props.titleLinkable !== void 0 ? props.titleLinkable : true;
13910
13950
  const showImage = isChildItem ? false : props.showImage !== void 0 ? props.showImage : true;
13911
13951
  const showSku = isChildItem ? false : props.showSku !== void 0 ? props.showSku : true;
@@ -14677,6 +14717,10 @@ function RequestPriceButton(props) {
14677
14717
  }
14678
14718
  var RequestPriceButton_default = RequestPriceButton;
14679
14719
  var ROW_SEQ = 0;
14720
+ function snapToStep(value, min, step) {
14721
+ if (!Number.isFinite(value) || value <= min) return min;
14722
+ return Math.round((value - min) / (step || 1)) * (step || 1) + min;
14723
+ }
14680
14724
  function blankRow() {
14681
14725
  ROW_SEQ += 1;
14682
14726
  return {
@@ -14688,6 +14732,7 @@ function blankRow() {
14688
14732
  grossPrice: 0,
14689
14733
  quantity: 1,
14690
14734
  minQuantity: 1,
14735
+ step: 1,
14691
14736
  matches: [],
14692
14737
  searching: false,
14693
14738
  searched: false
@@ -14781,6 +14826,7 @@ function QuickOrderInner(props) {
14781
14826
  grossPrice: match.grossPrice,
14782
14827
  quantity: match.minQuantity,
14783
14828
  minQuantity: match.minQuantity,
14829
+ step: match.step,
14784
14830
  matches: [],
14785
14831
  searching: false,
14786
14832
  searched: false
@@ -14793,7 +14839,7 @@ function QuickOrderInner(props) {
14793
14839
  const n = parseInt(raw, 10);
14794
14840
  setRows(
14795
14841
  (prev) => prev.map(
14796
- (r) => r.key === key ? { ...r, quantity: Number.isFinite(n) && n > 0 ? Math.max(r.minQuantity, n) : r.minQuantity } : r
14842
+ (r) => r.key === key ? { ...r, quantity: snapToStep(n, r.minQuantity, r.step) } : r
14797
14843
  )
14798
14844
  );
14799
14845
  },
@@ -14846,7 +14892,8 @@ function QuickOrderInner(props) {
14846
14892
  netPrice: exact.netPrice,
14847
14893
  grossPrice: exact.grossPrice,
14848
14894
  quantity: Math.max(exact.minQuantity, line.quantity),
14849
- minQuantity: exact.minQuantity
14895
+ minQuantity: exact.minQuantity,
14896
+ step: exact.step
14850
14897
  });
14851
14898
  }
14852
14899
  if (resolved.length) {
@@ -14977,7 +15024,7 @@ function QuickOrderInner(props) {
14977
15024
  {
14978
15025
  type: "number",
14979
15026
  min: r.minQuantity,
14980
- step: 1,
15027
+ step: r.step || 1,
14981
15028
  value: r.productId ? r.quantity : "",
14982
15029
  disabled: !r.productId,
14983
15030
  onChange: (e) => setQuantity(r.key, e.target.value),