@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/CHANGELOG.md CHANGED
@@ -8,6 +8,33 @@ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
8
  between minor versions; breaking changes are called out below and in
9
9
  [MIGRATION.md](./MIGRATION.md).
10
10
 
11
+ ## [0.27.0] - 2026-09-28
12
+
13
+ ### Fixed
14
+
15
+ - **A quantity field could not be cleared and retyped.** The typed-input handler
16
+ only committed a value once it was at or above the minimum, so with a minimum
17
+ of 2, backspacing to empty or typing "1" was ignored and the previous number
18
+ stayed stuck in the box. The field now accepts any keystroke while editing and
19
+ resolves to a valid quantity on blur. (PWP-970)
20
+
21
+ - **Incrementing an off-grid quantity kept it off-grid.** `increment()` added the
22
+ step without first snapping to the `min + n*step` grid, so a quantity that was
23
+ already off it never returned. Both steppers now snap first. (PWP-970)
24
+
25
+ - **Quick order ignored the order step.** Its quantity input was hardcoded to
26
+ `step={1}` and nothing snapped, while bulk paste and spreadsheet upload are the
27
+ primary ordering route for a wholesale account. `QuickOrderMatch` now carries
28
+ `step` alongside `minQuantity`, and typed quantities snap to it. (PWP-970)
29
+
30
+ - **`OrderItemCard` accepted a misconfiguration in silence.** It ships from the
31
+ RSC-safe `/pure` entry and reads no context, so the host must supply `language`
32
+ and `configuration.urls` — but both are optional, nothing warned, and the
33
+ failure only showed as a wrong-language name or a link that dropped the locale
34
+ prefix two clicks later. A development-only warning now names each omission
35
+ once. `titleLinkable` defaults to true, so the link case applies unless a host
36
+ opts out. (PWP-972)
37
+
11
38
  ## [0.26.1] - 2026-09-28
12
39
 
13
40
  ### Fixed
package/dist/index.cjs CHANGED
@@ -2634,6 +2634,7 @@ function toMatch(item, language) {
2634
2634
  const netPrice = displayItem?.price?.net ?? 0;
2635
2635
  const grossPrice = displayItem?.price?.gross ?? 0;
2636
2636
  const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2637
+ const step = Math.max(1, displayItem?.unit ?? 1);
2637
2638
  const imageUrl = isCluster ? propellerV2CoreUi.getClusterImageUrl(item) : propellerV2CoreUi.getProductImageUrl(item);
2638
2639
  return {
2639
2640
  productId,
@@ -2643,6 +2644,7 @@ function toMatch(item, language) {
2643
2644
  netPrice,
2644
2645
  grossPrice,
2645
2646
  minQuantity,
2647
+ step,
2646
2648
  imageUrl,
2647
2649
  priceOnRequest: isPriceOnRequest(item)
2648
2650
  };
@@ -4779,6 +4781,7 @@ function AddToCart(rawProps) {
4779
4781
  onCartCreated: props.onCartCreated
4780
4782
  });
4781
4783
  const [quantity, setQuantity] = React6.useState(() => getMinQuantity(props.product));
4784
+ const [quantityDraft, setQuantityDraft] = React6.useState(null);
4782
4785
  const [toastMessage, setToastMessage] = React6.useState(() => "");
4783
4786
  const [toastType, setToastType] = React6.useState(() => "");
4784
4787
  const [toastVisible, setToastVisible] = React6.useState(() => false);
@@ -4801,16 +4804,42 @@ function AddToCart(rawProps) {
4801
4804
  function dismissToast() {
4802
4805
  setToastVisible(false);
4803
4806
  }
4807
+ function snapQuantity(value) {
4808
+ const min = getMinQuantity(props.product);
4809
+ const step = getStep(props.product);
4810
+ if (value <= min) return min;
4811
+ return Math.round((value - min) / step) * step + min;
4812
+ }
4804
4813
  function increment() {
4805
- setQuantity(quantity + getStep(props.product));
4814
+ setQuantityDraft(null);
4815
+ const snapped = snapQuantity(quantity);
4816
+ setQuantity(snapped === quantity ? quantity + getStep(props.product) : snapped);
4806
4817
  }
4807
4818
  function decrement() {
4808
4819
  const min = getMinQuantity(props.product);
4809
4820
  const step = getStep(props.product);
4821
+ setQuantityDraft(null);
4822
+ const snapped = snapQuantity(quantity);
4823
+ if (snapped !== quantity) {
4824
+ setQuantity(snapped);
4825
+ return;
4826
+ }
4810
4827
  if (quantity - step >= min) {
4811
4828
  setQuantity(quantity - step);
4812
4829
  }
4813
4830
  }
4831
+ function handleQuantityInput(raw) {
4832
+ setQuantityDraft(raw);
4833
+ const val = parseInt(raw, 10);
4834
+ if (!isNaN(val) && val >= getMinQuantity(props.product)) {
4835
+ setQuantity(snapQuantity(val));
4836
+ }
4837
+ }
4838
+ function handleQuantityBlur() {
4839
+ const val = parseInt(quantityDraft ?? "", 10);
4840
+ setQuantity(isNaN(val) ? getMinQuantity(props.product) : snapQuantity(val));
4841
+ setQuantityDraft(null);
4842
+ }
4814
4843
  function getModalImageUrl() {
4815
4844
  if (addedCartItem) {
4816
4845
  const img = addedCartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url;
@@ -4940,15 +4969,9 @@ function AddToCart(rawProps) {
4940
4969
  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",
4941
4970
  min: getMinQuantity(props.product),
4942
4971
  step: getStep(props.product),
4943
- value: quantity,
4944
- onChange: (e) => {
4945
- const val = parseInt(e.target.value, 10);
4946
- const min = getMinQuantity(props.product);
4947
- const step = getStep(props.product);
4948
- if (!isNaN(val) && val >= min) {
4949
- setQuantity(Math.round((val - min) / step) * step + min);
4950
- }
4951
- }
4972
+ value: quantityDraft ?? quantity,
4973
+ onChange: (e) => handleQuantityInput(e.target.value),
4974
+ onBlur: handleQuantityBlur
4952
4975
  }
4953
4976
  ),
4954
4977
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4973,15 +4996,9 @@ function AddToCart(rawProps) {
4973
4996
  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",
4974
4997
  min: getMinQuantity(props.product),
4975
4998
  step: getStep(props.product),
4976
- value: quantity,
4977
- onChange: (e) => {
4978
- const val = parseInt(e.target.value, 10);
4979
- const min = getMinQuantity(props.product);
4980
- const step = getStep(props.product);
4981
- if (!isNaN(val) && val >= min) {
4982
- setQuantity(Math.round((val - min) / step) * step + min);
4983
- }
4984
- }
4999
+ value: quantityDraft ?? quantity,
5000
+ onChange: (e) => handleQuantityInput(e.target.value),
5001
+ onBlur: handleQuantityBlur
4985
5002
  }
4986
5003
  ) : null,
4987
5004
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -13903,6 +13920,28 @@ function OrderActions(rawProps) {
13903
13920
  );
13904
13921
  }
13905
13922
  var OrderActions_default = OrderActions;
13923
+ var warnedOnce = /* @__PURE__ */ new Set();
13924
+ function warnOnce(key, message) {
13925
+ if (warnedOnce.has(key)) return;
13926
+ warnedOnce.add(key);
13927
+ console.warn(message);
13928
+ }
13929
+ function warnMissingProps(props) {
13930
+ if (process.env.NODE_ENV === "production") return;
13931
+ if (!props.language) {
13932
+ warnOnce(
13933
+ "language",
13934
+ "[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."
13935
+ );
13936
+ }
13937
+ const linkable = props.titleLinkable !== void 0 ? props.titleLinkable : true;
13938
+ if (linkable && !props.configuration?.urls) {
13939
+ warnOnce(
13940
+ "urls",
13941
+ "[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}."
13942
+ );
13943
+ }
13944
+ }
13906
13945
  function getProductName3(orderItem, language) {
13907
13946
  return propellerV2CoreUi.getLocalizedValue(orderItem?.product?.names, language, "") || orderItem?.name || "Unknown Product";
13908
13947
  }
@@ -13926,6 +13965,7 @@ function getProductUrl(orderItem, language, urls) {
13926
13965
  function OrderItemCard(props) {
13927
13966
  const item = props.orderItem;
13928
13967
  const isChildItem = props.isChildItem || false;
13968
+ warnMissingProps(props);
13929
13969
  const titleLinkable = props.titleLinkable !== void 0 ? props.titleLinkable : true;
13930
13970
  const showImage = isChildItem ? false : props.showImage !== void 0 ? props.showImage : true;
13931
13971
  const showSku = isChildItem ? false : props.showSku !== void 0 ? props.showSku : true;
@@ -14697,6 +14737,10 @@ function RequestPriceButton(props) {
14697
14737
  }
14698
14738
  var RequestPriceButton_default = RequestPriceButton;
14699
14739
  var ROW_SEQ = 0;
14740
+ function snapToStep(value, min, step) {
14741
+ if (!Number.isFinite(value) || value <= min) return min;
14742
+ return Math.round((value - min) / (step || 1)) * (step || 1) + min;
14743
+ }
14700
14744
  function blankRow() {
14701
14745
  ROW_SEQ += 1;
14702
14746
  return {
@@ -14708,6 +14752,7 @@ function blankRow() {
14708
14752
  grossPrice: 0,
14709
14753
  quantity: 1,
14710
14754
  minQuantity: 1,
14755
+ step: 1,
14711
14756
  matches: [],
14712
14757
  searching: false,
14713
14758
  searched: false
@@ -14801,6 +14846,7 @@ function QuickOrderInner(props) {
14801
14846
  grossPrice: match.grossPrice,
14802
14847
  quantity: match.minQuantity,
14803
14848
  minQuantity: match.minQuantity,
14849
+ step: match.step,
14804
14850
  matches: [],
14805
14851
  searching: false,
14806
14852
  searched: false
@@ -14813,7 +14859,7 @@ function QuickOrderInner(props) {
14813
14859
  const n = parseInt(raw, 10);
14814
14860
  setRows(
14815
14861
  (prev) => prev.map(
14816
- (r) => r.key === key ? { ...r, quantity: Number.isFinite(n) && n > 0 ? Math.max(r.minQuantity, n) : r.minQuantity } : r
14862
+ (r) => r.key === key ? { ...r, quantity: snapToStep(n, r.minQuantity, r.step) } : r
14817
14863
  )
14818
14864
  );
14819
14865
  },
@@ -14866,7 +14912,8 @@ function QuickOrderInner(props) {
14866
14912
  netPrice: exact.netPrice,
14867
14913
  grossPrice: exact.grossPrice,
14868
14914
  quantity: Math.max(exact.minQuantity, line.quantity),
14869
- minQuantity: exact.minQuantity
14915
+ minQuantity: exact.minQuantity,
14916
+ step: exact.step
14870
14917
  });
14871
14918
  }
14872
14919
  if (resolved.length) {
@@ -14997,7 +15044,7 @@ function QuickOrderInner(props) {
14997
15044
  {
14998
15045
  type: "number",
14999
15046
  min: r.minQuantity,
15000
- step: 1,
15047
+ step: r.step || 1,
15001
15048
  value: r.productId ? r.quantity : "",
15002
15049
  disabled: !r.productId,
15003
15050
  onChange: (e) => setQuantity(r.key, e.target.value),