@propeller-commerce/propeller-v2-react-ui 0.28.0 → 0.30.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,88 @@ 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.30.0] - 2026-10-06
12
+
13
+ ### Added
14
+
15
+ - **`isPriceOnRequest` is exported from `/shared`.** It was only on the main
16
+ entry, so a Server Component could not ask "is this a quoted product?"
17
+ without drawing a client boundary — leaving server-rendered price blocks
18
+ unable to choose between an amount and the quote label. The function is plain
19
+ TS with no React, so `/shared` is where it belongs.
20
+
21
+ ### Fixed
22
+
23
+ - **`<SearchBar>` resolves its placeholder from `labels`.** It read
24
+ `props.placeholder` and fell straight back to the English
25
+ `'Search products...'`, never consulting the label bag every other string in
26
+ the component goes through. A host that passed only `labels` therefore got an
27
+ English placeholder in an otherwise translated header. The explicit prop
28
+ still wins, then `labels.placeholder`, then English.
29
+
30
+ ## [0.29.0] - 2026-10-05
31
+
32
+ ### Fixed
33
+
34
+ - **Listings no longer drop the company's orderlist on a client-side fetch.**
35
+ `useProductSearch` sent `applyOrderlists: false` whenever the caller passed
36
+ no explicit `orderlistIds` — which is every app — so the backend was told to
37
+ ignore the company's own orderlists. The server render omits the flag and is
38
+ correctly scoped, so a category page painted the right products and then the
39
+ first sort, filter, page or company change replaced them with the unscoped
40
+ catalogue, priced and orderable. The flag is now sent only when the caller
41
+ actually asks for it, in both the grid fetch and the search-bar preview.
42
+ `useQuickOrder` had the same default, which let a bulk paste resolve SKUs
43
+ outside the contract.
44
+
45
+ Callers that relied on the implicit `false` for an intentionally unscoped
46
+ listing must now pass `applyOrderlists={false}` explicitly.
47
+
48
+ - **`<ProductSlider>` is scoped to the viewer and priced for their company.**
49
+ `fetchProducts` sent no `userId`, no `companyId` and no
50
+ `priceCalculateProductInput`, and resolved its ids through the flat
51
+ `products` resolver, which does not apply orderlist scoping. A featured
52
+ slider therefore showed products outside the company's assortment at the
53
+ default company's prices. Its `filterToAssortment` step — whose only job is
54
+ dropping out-of-assortment crossupsells — asked the same resolver and so
55
+ could never filter anything. Both now go through `category.products` over
56
+ `configuration.baseCategoryId`, the path the grid and quick order already
57
+ use. Without a resolved base category the primary fetch returns nothing
58
+ rather than an unscoped list, while the crossupsell check falls through to
59
+ the unverified items so a slider is never blanked by a failed check.
60
+
61
+ - **Cart quantity steppers keep the line on the order grid.** `CartItem` read
62
+ `minimumQuantity` and the step but used them only to disable the decrement
63
+ button: the buttons added or subtracted the step from whatever was there, so
64
+ a line already off the grid stayed off it (min 2, step 3: 9 → 12). The typed
65
+ field committed only values at or above the minimum, so clearing it or typing
66
+ below the minimum left the old quantity stuck. Both steppers now resolve to
67
+ the grid first, and editing goes through a draft value that resolves on blur
68
+ — the same rule `<AddToCart>` and `<QuickOrder>` already followed.
69
+
70
+ - **Add to cart recovers when the remembered cart is gone.** A cart ordered in
71
+ another browser or deleted leaves its id behind in the host's storage, and
72
+ every later `cartAddItem` answered `CART_NOT_FOUND_ERROR`. Nothing forgot the
73
+ id, so each retry failed identically and the badge kept showing the dead
74
+ cart. `addItem` now forgets it and starts a fresh cart once, then re-adds;
75
+ the recovery is bounded because cart initialisation only ever returns an
76
+ `OPEN` cart, and it is skipped when the caller passed `createCart: false`.
77
+
78
+ - **`useProductInfo` no longer sends every company orderlist as an allowlist.**
79
+ It fetched all orderlists for the company and passed their ids to
80
+ `product(orderlistIds:)` with no filter on type, so a NEGATIVE (exclusion)
81
+ list was applied as an allowlist and inverted the filter. Scoping is left to
82
+ the backend, which merges positive and negative lists itself — one fewer
83
+ request per product page.
84
+
85
+ ### Changed
86
+
87
+ - **The min/step rule comes from `propeller-v2-core-ui`.** `AddToCart`,
88
+ `QuickOrder` and `CartItem` each carried their own copy, and they had
89
+ drifted — two of them divided by an unguarded step. All now call
90
+ `resolveOrderableQuantity` from core-ui 0.11.0, which is the new minimum for
91
+ this package.
92
+
11
93
  ## [0.28.0] - 2026-10-02
12
94
 
13
95
  ### Added
package/dist/index.cjs CHANGED
@@ -620,6 +620,14 @@ function useAuth(options) {
620
620
  }, [graphqlClient]);
621
621
  return { loading, error, login, magicLogin, createMagicToken, registerContact, registerCustomer, forgotPassword };
622
622
  }
623
+ var CART_NOT_FOUND = "CART_NOT_FOUND_ERROR";
624
+ function isCartNotFound(error) {
625
+ const entries = error?.errors;
626
+ if (!Array.isArray(entries)) return false;
627
+ return entries.some(
628
+ (e) => e?.extensions?.code === CART_NOT_FOUND || /cart:?\s*\S+\s+not found/i.test(e?.message ?? "")
629
+ );
630
+ }
623
631
  async function initCart(config) {
624
632
  const {
625
633
  services,
@@ -759,6 +767,11 @@ function useCart(options) {
759
767
  setCreatedCartId(c.cartId);
760
768
  cartIdRef.current = c.cartId;
761
769
  }, []);
770
+ const forgetCart = React6.useCallback(() => {
771
+ setCart(null);
772
+ setCreatedCartId("");
773
+ cartIdRef.current = "";
774
+ }, []);
762
775
  const imageSearchFilters = React6.useCallback(
763
776
  () => configuration?.imageSearchFiltersGrid ?? DEFAULT_IMAGE_SEARCH_FILTERS,
764
777
  [configuration]
@@ -842,13 +855,28 @@ function useCart(options) {
842
855
  } else return propellerV2CoreUi.err("No cart ID provided");
843
856
  }
844
857
  const service = propellerV2CoreUi.createServices(graphqlClient).cart;
845
- const resultCart = await service.addItemToCart({
846
- id: resolvedCartId,
847
- input: { productId: opts.product.productId, quantity: opts.quantity, ...opts.cluster?.clusterId !== void 0 && { clusterId: opts.cluster.clusterId }, ...childItemInputs && { childItems: childItemInputs }, ...opts.notes && { notes: opts.notes }, ...opts.price !== void 0 && { price: opts.price } },
848
- language,
849
- imageSearchFilters: imageSearchFilters(),
850
- imageVariantFilters: imageVariantFilters()
851
- });
858
+ const input = { productId: opts.product.productId, quantity: opts.quantity, ...opts.cluster?.clusterId !== void 0 && { clusterId: opts.cluster.clusterId }, ...childItemInputs && { childItems: childItemInputs }, ...opts.notes && { notes: opts.notes }, ...opts.price !== void 0 && { price: opts.price } };
859
+ let resultCart;
860
+ try {
861
+ resultCart = await service.addItemToCart({
862
+ id: resolvedCartId,
863
+ input,
864
+ language,
865
+ imageSearchFilters: imageSearchFilters(),
866
+ imageVariantFilters: imageVariantFilters()
867
+ });
868
+ } catch (e) {
869
+ if (!isCartNotFound(e) || !opts.createCart) throw e;
870
+ forgetCart();
871
+ const fresh = await resolveCart();
872
+ resultCart = await service.addItemToCart({
873
+ id: fresh.cartId,
874
+ input,
875
+ language,
876
+ imageSearchFilters: imageSearchFilters(),
877
+ imageVariantFilters: imageVariantFilters()
878
+ });
879
+ }
852
880
  rememberCart(resultCart);
853
881
  const addedItem = resultCart.items?.find((i) => i.productId === opts.product.productId) ?? null;
854
882
  opts.afterAddToCart?.(resultCart, addedItem);
@@ -860,7 +888,7 @@ function useCart(options) {
860
888
  } finally {
861
889
  setLoading(false);
862
890
  }
863
- }, [graphqlClient, cartId, language, configuration, resolveCart, rememberCart, imageSearchFilters, imageVariantFilters]);
891
+ }, [graphqlClient, cartId, language, configuration, resolveCart, rememberCart, forgetCart, imageSearchFilters, imageVariantFilters]);
864
892
  const addItems = React6.useCallback(async (items) => {
865
893
  const added = [];
866
894
  let lastCart = null;
@@ -2146,19 +2174,8 @@ function useProductInfo(options) {
2146
2174
  setLoading(true);
2147
2175
  setError(null);
2148
2176
  try {
2149
- let orderlistIds = [];
2150
- let applyOrderlists = true;
2151
- if (options.orderlistIds && options.orderlistIds.length > 0) {
2152
- orderlistIds = options.orderlistIds;
2153
- applyOrderlists = options.applyOrderlists !== false;
2154
- } else if (options.applyOrderlists === false) {
2155
- applyOrderlists = false;
2156
- } else if (options.user && options.companyId) {
2157
- const orderlistService = propellerV2CoreUi.createServices(graphqlClient).orderlist;
2158
- const searchInput = { companyIds: [options.companyId] };
2159
- const orderlists = await orderlistService.getOrderlists(searchInput);
2160
- orderlistIds = (orderlists?.items ?? []).map((ol) => ol.id);
2161
- }
2177
+ const orderlistIds = options.orderlistIds ?? [];
2178
+ const applyOrderlists = options.applyOrderlists !== false;
2162
2179
  const service = propellerV2CoreUi.createServices(graphqlClient).product;
2163
2180
  const attributeInput = buildAttributeInput2();
2164
2181
  const variables = {
@@ -2369,7 +2386,7 @@ function useProductSearch(options) {
2369
2386
  const orderlistScope = options.orderlistIds && options.orderlistIds.length > 0 ? {
2370
2387
  applyOrderlists: options.applyOrderlists !== false,
2371
2388
  orderlistIds: options.orderlistIds
2372
- } : { applyOrderlists: false };
2389
+ } : options.applyOrderlists === false ? { applyOrderlists: false } : {};
2373
2390
  const categoryProductSearchInput = {
2374
2391
  language: lang,
2375
2392
  page: pagination.currentPage,
@@ -2499,7 +2516,7 @@ function useProductSearch(options) {
2499
2516
  const orderlistScope = options.orderlistIds && options.orderlistIds.length > 0 ? {
2500
2517
  applyOrderlists: options.applyOrderlists !== false,
2501
2518
  orderlistIds: options.orderlistIds
2502
- } : { applyOrderlists: false };
2519
+ } : options.applyOrderlists === false ? { applyOrderlists: false } : {};
2503
2520
  const userId = resolveListingUserId(options.user, configuration);
2504
2521
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2505
2522
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
@@ -2675,7 +2692,7 @@ function useQuickOrder(options) {
2675
2692
  if (!catId) return [];
2676
2693
  try {
2677
2694
  const service = propellerV2CoreUi.createServices(graphqlClient).category;
2678
- const orderlistScope = orderlistIds && orderlistIds.length > 0 ? { applyOrderlists: applyOrderlists !== false, orderlistIds } : { applyOrderlists: false };
2695
+ const orderlistScope = orderlistIds && orderlistIds.length > 0 ? { applyOrderlists: applyOrderlists !== false, orderlistIds } : applyOrderlists === false ? { applyOrderlists: false } : {};
2679
2696
  const userId = resolveListingUserId(user, configuration);
2680
2697
  const contactId = user && "contactId" in user ? user.contactId : void 0;
2681
2698
  const customerId = user && "customerId" in user ? user.customerId : void 0;
@@ -3284,28 +3301,33 @@ function useProductSlider(options) {
3284
3301
  }
3285
3302
  }
3286
3303
  if (!productIds.length && !clusterIds.length) return items;
3304
+ const catId = configuration.baseCategoryId;
3305
+ if (!catId) return items;
3287
3306
  const userId = resolveUserId();
3288
3307
  const companyId = resolveCompanyId();
3289
- const searchInput = {
3308
+ const categoryProductSearchInput = {
3290
3309
  ...productIds.length && { productIds },
3291
3310
  ...clusterIds.length && { clusterIds },
3292
3311
  language,
3293
3312
  page: 1,
3294
3313
  offset: 50,
3295
3314
  statuses: [propellerSdkV2.ProductStatus.A, propellerSdkV2.ProductStatus.P, propellerSdkV2.ProductStatus.T, propellerSdkV2.ProductStatus.S],
3315
+ hidden: false,
3296
3316
  ...userId !== void 0 && { userId },
3297
3317
  ...companyId !== void 0 && { companyId }
3298
3318
  };
3299
3319
  const filterAvailableAttributeInput = { isSearchable: true };
3300
3320
  const variables = {
3301
- input: searchInput,
3321
+ categoryId: catId,
3322
+ language,
3323
+ categoryProductSearchInput,
3324
+ filterAvailableAttributeInput,
3302
3325
  imageSearchFilters: configuration.imageSearchFiltersGrid,
3303
- imageVariantFilters: configuration.imageVariantFiltersMedium,
3304
- filterAvailableAttributeInput
3326
+ imageVariantFilters: configuration.imageVariantFiltersMedium
3305
3327
  };
3306
3328
  try {
3307
- const response = await propellerV2CoreUi.createServices(graphqlClient).product.getProducts(variables);
3308
- const resolved = response?.items ?? [];
3329
+ const response = await propellerV2CoreUi.createServices(graphqlClient).category.getCategory(variables);
3330
+ const resolved = response?.products?.items ?? [];
3309
3331
  const allowed = /* @__PURE__ */ new Set();
3310
3332
  for (const r of resolved) {
3311
3333
  const id = r.productId ?? r.clusterId;
@@ -3361,10 +3383,17 @@ function useProductSlider(options) {
3361
3383
  setLoading(true);
3362
3384
  setError(null);
3363
3385
  try {
3364
- const service = propellerV2CoreUi.createServices(graphqlClient).product;
3365
- const searchInput = {
3366
- productIds,
3367
- clusterIds,
3386
+ const catId = configuration.baseCategoryId;
3387
+ if (!catId) {
3388
+ setProducts([]);
3389
+ return;
3390
+ }
3391
+ const service = propellerV2CoreUi.createServices(graphqlClient).category;
3392
+ const userId = resolveUserId();
3393
+ const companyId = resolveCompanyId();
3394
+ const categoryProductSearchInput = {
3395
+ ...productIds.length && { productIds },
3396
+ ...clusterIds.length && { clusterIds },
3368
3397
  language,
3369
3398
  page: 1,
3370
3399
  offset: 50,
@@ -3373,17 +3402,24 @@ function useProductSlider(options) {
3373
3402
  propellerSdkV2.ProductStatus.P,
3374
3403
  propellerSdkV2.ProductStatus.T,
3375
3404
  propellerSdkV2.ProductStatus.S
3376
- ]
3405
+ ],
3406
+ hidden: false,
3407
+ ...userId !== void 0 && { userId },
3408
+ ...companyId !== void 0 && { companyId }
3377
3409
  };
3378
3410
  const filterAvailableAttributeInput = { isSearchable: true };
3379
3411
  const variables = {
3380
- input: searchInput,
3412
+ categoryId: catId,
3413
+ language,
3414
+ categoryProductSearchInput,
3415
+ priceCalculateProductInput: buildPriceInput(),
3416
+ filterAvailableAttributeInput,
3381
3417
  imageSearchFilters: configuration.imageSearchFiltersGrid,
3382
- imageVariantFilters: configuration.imageVariantFiltersMedium,
3383
- filterAvailableAttributeInput
3418
+ imageVariantFilters: configuration.imageVariantFiltersMedium
3384
3419
  };
3385
- const response = await service.getProducts(variables);
3386
- setProducts(response?.items ?? []);
3420
+ const response = await service.getCategory(variables);
3421
+ const productsResponse = response?.products;
3422
+ setProducts(productsResponse?.items ?? []);
3387
3423
  } catch (e) {
3388
3424
  setError(e instanceof Error ? e.message : "Failed to fetch products");
3389
3425
  setProducts([]);
@@ -4827,10 +4863,7 @@ function AddToCart(rawProps) {
4827
4863
  setToastVisible(false);
4828
4864
  }
4829
4865
  function snapQuantity(value) {
4830
- const min = getMinQuantity(props.product);
4831
- const step = getStep(props.product);
4832
- if (value <= min) return min;
4833
- return Math.round((value - min) / step) * step + min;
4866
+ return propellerV2CoreUi.resolveOrderableQuantity(value, getMinQuantity(props.product), getStep(props.product));
4834
4867
  }
4835
4868
  function increment() {
4836
4869
  setQuantityDraft(null);
@@ -6939,6 +6972,7 @@ function CartItem(rawProps) {
6939
6972
  companyId: props.companyId
6940
6973
  });
6941
6974
  const [quantity, setQuantity] = React6.useState(() => props.cartItem.quantity || 1);
6975
+ const [quantityDraft, setQuantityDraft] = React6.useState(() => null);
6942
6976
  const [notes, setNotes] = React6.useState(() => props.cartItem.notes || "");
6943
6977
  const [loading, setLoading] = React6.useState(() => false);
6944
6978
  const [deleting, setDeleting] = React6.useState(() => false);
@@ -7062,6 +7096,34 @@ function CartItem(rawProps) {
7062
7096
  setLoading(false);
7063
7097
  }
7064
7098
  }
7099
+ function handleQuantityDecrement() {
7100
+ setQuantityDraft(null);
7101
+ const orderable = propellerV2CoreUi.resolveOrderableQuantity(quantity, minQuantity, step);
7102
+ if (orderable !== quantity) {
7103
+ handleQuantityChange(orderable);
7104
+ return;
7105
+ }
7106
+ if (quantity - step >= minQuantity) handleQuantityChange(quantity - step);
7107
+ }
7108
+ function handleQuantityIncrement() {
7109
+ setQuantityDraft(null);
7110
+ const orderable = propellerV2CoreUi.resolveOrderableQuantity(quantity, minQuantity, step);
7111
+ handleQuantityChange(orderable === quantity ? quantity + step : orderable);
7112
+ }
7113
+ function handleQuantityInput(raw) {
7114
+ setQuantityDraft(raw);
7115
+ const val = parseInt(raw, 10);
7116
+ if (!isNaN(val) && val >= minQuantity) {
7117
+ handleQuantityChange(propellerV2CoreUi.resolveOrderableQuantity(val, minQuantity, step));
7118
+ }
7119
+ }
7120
+ function handleQuantityBlur() {
7121
+ if (quantityDraft === null) return;
7122
+ const val = parseInt(quantityDraft, 10);
7123
+ setQuantityDraft(null);
7124
+ const orderable = propellerV2CoreUi.resolveOrderableQuantity(val, minQuantity, step);
7125
+ if (orderable !== quantity) handleQuantityChange(orderable);
7126
+ }
7065
7127
  function handleNoteChange(note) {
7066
7128
  setNotes(note);
7067
7129
  if (props.onNoteChange) {
@@ -7164,7 +7226,8 @@ function CartItem(rawProps) {
7164
7226
  deleting,
7165
7227
  crossupsells,
7166
7228
  visibleCrossupsells: getVisibleCrossupsells(),
7167
- addingCrossupsellId
7229
+ addingCrossupsellId,
7230
+ quantityDraft
7168
7231
  },
7169
7232
  helpers: {
7170
7233
  getBundleItemName,
@@ -7177,6 +7240,10 @@ function CartItem(rawProps) {
7177
7240
  },
7178
7241
  handlers: {
7179
7242
  onQuantityChange: handleQuantityChange,
7243
+ onQuantityDecrement: handleQuantityDecrement,
7244
+ onQuantityIncrement: handleQuantityIncrement,
7245
+ onQuantityInput: handleQuantityInput,
7246
+ onQuantityBlur: handleQuantityBlur,
7180
7247
  onNoteChange: handleNoteChange,
7181
7248
  onDelete: handleDelete,
7182
7249
  onTitleClick: (e) => props.onTitleClick?.(e, props.cartItem),
@@ -7491,7 +7558,7 @@ function CartItemQuantity(props = {}) {
7491
7558
  {
7492
7559
  type: "button",
7493
7560
  className: "propeller-cart-item__decrement px-2.5 h-full text-muted-foreground hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed transition-colors rounded-l-control select-none",
7494
- onClick: () => handlers.onQuantityChange(derived.quantity - derived.step),
7561
+ onClick: handlers.onQuantityDecrement,
7495
7562
  disabled: derived.quantity <= derived.minQuantity || state.loading,
7496
7563
  children: "-"
7497
7564
  }
@@ -7503,13 +7570,9 @@ function CartItemQuantity(props = {}) {
7503
7570
  className: "propeller-cart-item__quantity w-10 text-center text-sm bg-transparent border-x border-input h-full focus:ring-0 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
7504
7571
  min: derived.minQuantity,
7505
7572
  step: derived.step,
7506
- value: derived.quantity,
7507
- onChange: (e) => {
7508
- const val = parseInt(e.target.value, 10);
7509
- if (!isNaN(val) && val >= derived.minQuantity) {
7510
- handlers.onQuantityChange(Math.round((val - derived.minQuantity) / derived.step) * derived.step + derived.minQuantity);
7511
- }
7512
- }
7573
+ value: state.quantityDraft ?? derived.quantity,
7574
+ onChange: (e) => handlers.onQuantityInput(e.target.value),
7575
+ onBlur: handlers.onQuantityBlur
7513
7576
  }
7514
7577
  ),
7515
7578
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -7517,7 +7580,7 @@ function CartItemQuantity(props = {}) {
7517
7580
  {
7518
7581
  type: "button",
7519
7582
  className: "propeller-cart-item__increment px-2.5 h-full text-muted-foreground hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed transition-colors rounded-r-control select-none",
7520
- onClick: () => handlers.onQuantityChange(derived.quantity + derived.step),
7583
+ onClick: handlers.onQuantityIncrement,
7521
7584
  disabled: state.loading,
7522
7585
  children: "+"
7523
7586
  }
@@ -7531,13 +7594,9 @@ function CartItemQuantity(props = {}) {
7531
7594
  className: props.className ?? "propeller-cart-item__quantity w-14 h-9 text-center text-sm border border-input rounded-control focus:ring-2 focus:ring-primary focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
7532
7595
  min: derived.minQuantity,
7533
7596
  step: derived.step,
7534
- value: derived.quantity,
7535
- onChange: (e) => {
7536
- const val = parseInt(e.target.value, 10);
7537
- if (!isNaN(val) && val >= derived.minQuantity) {
7538
- handlers.onQuantityChange(Math.round((val - derived.minQuantity) / derived.step) * derived.step + derived.minQuantity);
7539
- }
7540
- }
7597
+ value: state.quantityDraft ?? derived.quantity,
7598
+ onChange: (e) => handlers.onQuantityInput(e.target.value),
7599
+ onBlur: handlers.onQuantityBlur
7541
7600
  }
7542
7601
  );
7543
7602
  }
@@ -14795,10 +14854,6 @@ function RequestPriceButton(props) {
14795
14854
  }
14796
14855
  var RequestPriceButton_default = RequestPriceButton;
14797
14856
  var ROW_SEQ = 0;
14798
- function snapToStep(value, min, step) {
14799
- if (!Number.isFinite(value) || value <= min) return min;
14800
- return Math.round((value - min) / (step || 1)) * (step || 1) + min;
14801
- }
14802
14857
  function blankRow() {
14803
14858
  ROW_SEQ += 1;
14804
14859
  return {
@@ -14918,7 +14973,7 @@ function QuickOrderInner(props) {
14918
14973
  const n = parseInt(raw, 10);
14919
14974
  setRows(
14920
14975
  (prev) => prev.map(
14921
- (r) => r.key === key ? { ...r, quantity: snapToStep(n, r.minQuantity, r.step) } : r
14976
+ (r) => r.key === key ? { ...r, quantity: propellerV2CoreUi.resolveOrderableQuantity(n, r.minQuantity, r.step) } : r
14922
14977
  )
14923
14978
  );
14924
14979
  },
@@ -19489,7 +19544,7 @@ function SearchBar(rawProps) {
19489
19544
  "propeller-search-bar__input w-full pl-10 pr-10 py-2 bg-card text-foreground border border-input rounded-container focus:outline-none focus:ring-2 focus:ring-secondary placeholder:text-muted-foreground",
19490
19545
  props.inputClassName
19491
19546
  ),
19492
- placeholder: props.placeholder || "Search products...",
19547
+ placeholder: props.placeholder || propellerV2CoreUi.getLabel(props.labels, "placeholder", "Search products..."),
19493
19548
  value: localTerm,
19494
19549
  onChange: (e) => handleInputChange(e.target.value)
19495
19550
  }