@propeller-commerce/propeller-v2-react-ui 0.19.2 → 0.20.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,87 @@ 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.20.0] - 2026-09-21
12
+
13
+ ### Fixed
14
+
15
+ - `FavoriteListDetails` and `useCart.getCrossupsells` priced items for the
16
+ contact's default company instead of the company selected in the switcher, so
17
+ a multi-company contact saw the wrong prices while the cart charged the
18
+ selected company's. Both now resolve the active company as
19
+ `companyId ?? user.company.companyId`, matching `useProductSlider`.
20
+ - `FavoriteListDetails` and `CartItem` fetched only on mount, leaving stale
21
+ prices on screen after a company switch. Both now re-fetch when `companyId`
22
+ changes.
23
+
24
+ - The closed `CartIconAndSidebar` panel widened the document by its own content
25
+ width (~128px once the cart had items), giving signed-in users a stray
26
+ horizontal scrollbar. A transform does not remove an element from scrollable
27
+ overflow, so the parked drawer is now `visibility: hidden` while closed.
28
+ (PWP-1002)
29
+ - `ProductDescription`, `ProductShortDescription`, `CategoryDescription` and
30
+ `CategoryShortDescription` emitted Tailwind Typography `prose` classes, but
31
+ the plugin is installed neither here nor in the consuming apps — so the
32
+ classes compiled to nothing while this package's own Preflight stripped list
33
+ markers, and bullet lists rendered as bare lines. The four surfaces now carry
34
+ real styling in `dist/styles.css`, with no consumer plugin required.
35
+ (PWP-1000)
36
+ - `ItemStock` rendered `(120pcs)` with no separator between the count and the
37
+ unit label. (PWP-1005a)
38
+ - `OrderList` filter labels carried Tailwind's `capitalize`, which title-cases
39
+ every word and mangled translated labels ("Terme De Recherche",
40
+ "Aangemaakt Op"). The dictionaries already carry the right casing.
41
+ (PWP-1005b)
42
+ - `AccountIconAndMenu` hardcoded a dark-header scheme (`text-white`,
43
+ `hover:bg-white/10`) that rendered white-on-light, and styled its active
44
+ sidebar link with the `secondary` ramp where every other selected surface in
45
+ the package uses `primary`. Both now use theme tokens. (PWP-1005c)
46
+
47
+ ### Added
48
+
49
+ - `FavoriteListDetails` accepts a `companyId` prop, resolved from
50
+ `PropellerProvider` when omitted.
51
+ - `AccountIconAndMenu` accepts a `linkClassName` prop for its sidebar links,
52
+ which previously had no class escape hatch. (PWP-1005c)
53
+
54
+ - The purchase-authorization rule existed in three divergent copies — `useCart`
55
+ compared `<=`, `CartSummary` and `CartIconAndSidebar` compared `>`, and only
56
+ `CartSummary` coerced `companyId` numerically, so a string company id from a
57
+ cookie matched in one place and not the others. All three now call
58
+ `isOverAuthorizationLimit` from core-ui.
59
+
60
+ This also fixes the hook failing open: `useCart.checkoutAllowed` returned
61
+ `true` when its own internal cart was null, which it is until the consumer
62
+ calls `addItem`/`resolveCart` — so an app reading it for a cart it fetched
63
+ itself was told an over-limit cart could check out. (PWP-988)
64
+
65
+ ### Removed
66
+
67
+ - **Breaking:** `ProductGrid.renderProductCard` and `renderClusterCard`. Both
68
+ were declared in the public type but never invoked — only tested for
69
+ truthiness, which suppressed the default card and rendered nothing. Setting
70
+ either produced an empty grid. They were superseded by the compound-component
71
+ work that introduced `productCardComponent` / `clusterCardComponent`; the call
72
+ that used them was replaced then, and only the guard survived. Use
73
+ `productCardComponent` / `clusterCardComponent` to swap a whole card, or
74
+ `ProductGrid.Items.renderItem` for per-item control. No working usage was
75
+ possible, so nothing that functioned is lost. (PWP-985)
76
+
77
+ ### Changed
78
+
79
+ - `ProductDownloads.downloads` and `ProductVideos.videos` are now optional and
80
+ nullable, matching what the API actually returns — it answers `null`, not an
81
+ empty page, when a product has no documents or videos. Both components
82
+ already handled the absent case; only the types disagreed, which forced casts
83
+ at the `ProductTabs` call sites. Those are gone. (PWP-1009)
84
+ - `GridPagination.products` is now structural (`{ page, pages }`) instead of a
85
+ full `ProductsResponse`. A real response still fits; callers holding the two
86
+ numbers on their own no longer need a cast. (PWP-1005e)
87
+ - **Breaking (types):** `GridToolbar.viewMode` and `onViewChange` are narrowed
88
+ from `string` to `'grid' | 'list'`, matching their documented contract. A
89
+ consumer passing a plain `string` will need a cast or a narrower type.
90
+ (PWP-1005d)
91
+
11
92
  ## [0.19.2] - 2026-08-27
12
93
 
13
94
  ### Fixed
package/dist/index.cjs CHANGED
@@ -745,6 +745,7 @@ var DEFAULT_IMAGE_VARIANT_FILTERS = {
745
745
  function useCart(options) {
746
746
  const { graphqlClient, user, companyId, configuration, onCartCreated } = options;
747
747
  const language = options.language || configuration?.language || "NL";
748
+ const resolvedCompanyId = companyId ?? (user && "contactId" in user ? user.company?.companyId : void 0);
748
749
  const [cart, setCart] = React6.useState(null);
749
750
  const [createdCartId, setCreatedCartId] = React6.useState("");
750
751
  const cartId = options.cartId || createdCartId;
@@ -766,20 +767,10 @@ function useCart(options) {
766
767
  const [loading, setLoading] = React6.useState(false);
767
768
  const [error, setError] = React6.useState(null);
768
769
  const notesTimers = React6.useRef({});
769
- const checkoutAllowed = React6.useMemo(() => {
770
- if (!user || !("contactId" in user)) return true;
771
- if (!companyId) return true;
772
- if (!cart) return true;
773
- const pacData = user.purchaseAuthorizationConfigs;
774
- const items = pacData?.items ?? [];
775
- const purchaserPac = items.find(
776
- (pac) => pac.purchaseRole === propellerSdkV2.PurchaseRole.PURCHASER && pac.company?.companyId === companyId
777
- );
778
- if (!purchaserPac) return true;
779
- const limit = purchaserPac.authorizationLimit ?? 0;
780
- const totalGross = cart.total?.totalGross ?? 0;
781
- return totalGross <= limit;
782
- }, [user, companyId, cart]);
770
+ const checkoutAllowed = React6.useMemo(
771
+ () => propellerV2CoreUi.isCheckoutAllowed(user, companyId, cart),
772
+ [user, companyId, cart]
773
+ );
783
774
  function getMinQuantity(product) {
784
775
  const min = product?.minimumQuantity;
785
776
  return min && min > 0 ? min : 1;
@@ -968,7 +959,8 @@ function useCart(options) {
968
959
  imageVariantFilters: opts.imageVariantFilters ?? imageVariantFilters(),
969
960
  priceCalculateProductInput: {
970
961
  taxZone: taxZone || "NL",
971
- ...user && "company" in user && { companyId: user?.company?.companyId },
962
+ // Switcher selection wins; the contact's default company is the fallback.
963
+ ...resolvedCompanyId !== void 0 && { companyId: resolvedCompanyId },
972
964
  ...user && "contactId" in user && { contactId: user?.contactId },
973
965
  ...user && "customerId" in user && { customerId: user?.customerId }
974
966
  }
@@ -978,7 +970,7 @@ function useCart(options) {
978
970
  } catch {
979
971
  return [];
980
972
  }
981
- }, [graphqlClient, language, user, imageSearchFilters, imageVariantFilters]);
973
+ }, [graphqlClient, language, user, resolvedCompanyId, imageSearchFilters, imageVariantFilters]);
982
974
  return { cart, cartId, loading, error, checkoutAllowed, resolveCart, addItem, addItems, updateItemQuantity, updateItemNotes, deleteItem, addActionCode, removeActionCode, requestAuthorization, processCart, getCrossupsells, getMinQuantity, getStep };
983
975
  }
984
976
  function buildAddressInput(type, addr) {
@@ -4211,7 +4203,9 @@ function AccountIconAndMenu(rawProps) {
4211
4203
  href: link.href,
4212
4204
  onClick: (event) => handleMenuLinkClick(event, link.href),
4213
4205
  "data-active": isActiveLink(link.href) ? "true" : "false",
4214
- className: `propeller-account-menu__link flex w-full items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors ${isActiveLink(link.href) ? "bg-secondary/5 text-secondary border-l-2 border-secondary" : "text-muted-foreground hover:bg-surface-hover hover:text-foreground"}`,
4206
+ className: cn(
4207
+ `propeller-account-menu__link flex w-full items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors ${isActiveLink(link.href) ? "bg-primary/5 text-primary border-l-2 border-primary" : "text-muted-foreground hover:bg-surface-hover hover:text-foreground"}${props.linkClassName ? " " + props.linkClassName : ""}`
4208
+ ),
4215
4209
  children: link.label
4216
4210
  }
4217
4211
  ) }, link.href)) }) }),
@@ -4233,7 +4227,7 @@ function AccountIconAndMenu(rawProps) {
4233
4227
  onClick: (event) => handleIconClick(),
4234
4228
  "aria-label": propellerV2CoreUi.getLabel(props.labels, "accountLabel", "Account"),
4235
4229
  "data-open": menuOpen ? "true" : "false",
4236
- className: cn(`propeller-account-menu__trigger inline-flex items-center gap-2 px-3 py-2 rounded-control text-sm font-medium transition-colors text-white hover:bg-white/10${props.iconClassName ? " " + props.iconClassName : ""}`),
4230
+ className: cn(`propeller-account-menu__trigger inline-flex items-center gap-2 px-3 py-2 rounded-control text-sm font-medium transition-colors text-foreground hover:bg-surface-hover${props.iconClassName ? " " + props.iconClassName : ""}`),
4237
4231
  children: [
4238
4232
  /* @__PURE__ */ jsxRuntime.jsx(
4239
4233
  "svg",
@@ -6631,6 +6625,7 @@ function ItemStock(props) {
6631
6625
  getTotalQuantity() > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-item-stock__count opacity-70", children: [
6632
6626
  "(",
6633
6627
  getTotalQuantity(),
6628
+ " ",
6634
6629
  propellerV2CoreUi.getLabel(props.labels, "pieces", "pcs"),
6635
6630
  ")"
6636
6631
  ] }) : null
@@ -6864,7 +6859,7 @@ function CartItem(rawProps) {
6864
6859
  }
6865
6860
  React6.useEffect(() => {
6866
6861
  fetchCrossupsells();
6867
- }, []);
6862
+ }, [props.companyId]);
6868
6863
  React6.useEffect(() => {
6869
6864
  setQuantity(props.cartItem.quantity || 1);
6870
6865
  setNotes(props.cartItem.notes || "");
@@ -7318,13 +7313,6 @@ CartItemExport.Price = CartItemPrice;
7318
7313
  CartItemExport.Quantity = CartItemQuantity;
7319
7314
  CartItemExport.Delete = CartItemDelete;
7320
7315
  var CartItem_default = CartItemExport;
7321
- function findPurchaserPAC(user, companyId) {
7322
- if (!user || !("contactId" in user) || !companyId) return void 0;
7323
- const items = user.purchaseAuthorizationConfigs?.items ?? [];
7324
- return items.find(
7325
- (pac) => pac.purchaseRole === propellerSdkV2.PurchaseRole.PURCHASER && pac.company?.companyId === companyId
7326
- );
7327
- }
7328
7316
  function CartIconAndSidebar(rawProps) {
7329
7317
  const props = useInfraProps(rawProps);
7330
7318
  const CartBonusItemsImpl = props.cartBonusItemsComponent ?? CartBonusItems_default;
@@ -7355,10 +7343,9 @@ function CartIconAndSidebar(rawProps) {
7355
7343
  return useTax ? propellerV2CoreUi.getLabel(props.labels, key, "Total") : propellerV2CoreUi.getLabel(props.labels, "totalExclVat", "Total excl. VAT");
7356
7344
  }
7357
7345
  const sidebarTitle = props.cartSidebarTitle || props.labels?.["cartSidebarTitle"] || "Shopping cart";
7358
- const purchaserPAC = findPurchaserPAC(props.user, props.companyId);
7359
- const totalGross = props.cart?.total?.totalGross ?? 0;
7360
- const overLimit = !!purchaserPAC && totalGross > (purchaserPAC.authorizationLimit ?? 0);
7361
- const showCheckoutButton = props.cartCheckoutButton === false ? false : !props.user || !("contactId" in props.user) || !props.companyId || !purchaserPAC ? true : !overLimit;
7346
+ const purchaserPAC = propellerV2CoreUi.findPurchaserPac(props.user, props.companyId);
7347
+ const overLimit = propellerV2CoreUi.isOverAuthorizationLimit(props.user, props.companyId, props.cart);
7348
+ const showCheckoutButton = props.cartCheckoutButton === false ? false : !purchaserPAC ? true : !overLimit;
7362
7349
  const showRequestAuthorizationButton = !!purchaserPAC && overLimit;
7363
7350
  function handleIconClick() {
7364
7351
  if (props.showCartSidebarOnClick !== false) {
@@ -7971,17 +7958,7 @@ function CartSummary(rawProps) {
7971
7958
  }
7972
7959
  }
7973
7960
  function showRequestAuthorizationButton() {
7974
- if (!props.user || !("contactId" in props.user)) return false;
7975
- if (!props.companyId) return false;
7976
- if (!props.cart) return false;
7977
- const items = props.user.purchaseAuthorizationConfigs?.items ?? [];
7978
- const purchaserPAC = items.find(
7979
- (pac) => pac.purchaseRole === propellerSdkV2.PurchaseRole.PURCHASER && Number(pac.company?.companyId) === Number(props.companyId)
7980
- );
7981
- if (!purchaserPAC) return false;
7982
- const limit = purchaserPAC.authorizationLimit ?? 0;
7983
- const totalGross = props.cart.total?.totalGross ?? 0;
7984
- return totalGross > limit;
7961
+ return propellerV2CoreUi.isOverAuthorizationLimit(props.user, props.companyId, props.cart);
7985
7962
  }
7986
7963
  function showRequestQuoteButton() {
7987
7964
  return !!props.onRequestQuoteClick && !!props.user && "contactId" in props.user;
@@ -8121,7 +8098,7 @@ function CategoryDescription(props) {
8121
8098
  !shouldTruncate || expanded ? /* @__PURE__ */ jsxRuntime.jsx(
8122
8099
  "div",
8123
8100
  {
8124
- className: "propeller-category-description__content prose prose-slate max-w-none text-muted-foreground",
8101
+ className: "propeller-category-description__content text-muted-foreground",
8125
8102
  dangerouslySetInnerHTML: { __html: html }
8126
8103
  }
8127
8104
  ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "propeller-category-description__truncated text-muted-foreground", children: getTruncated() }),
@@ -8147,7 +8124,7 @@ function CategoryShortDescription(props) {
8147
8124
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !!html ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(`propeller-category-short-description mb-6 ${props.className || ""}`), children: /* @__PURE__ */ jsxRuntime.jsx(
8148
8125
  "div",
8149
8126
  {
8150
- className: "propeller-category-short-description__content prose prose-slate max-w-none text-muted-foreground",
8127
+ className: "propeller-category-short-description__content text-muted-foreground",
8151
8128
  dangerouslySetInnerHTML: { __html: html }
8152
8129
  }
8153
8130
  ) }) : null });
@@ -9387,27 +9364,24 @@ function ProductGrid(rawProps) {
9387
9364
  {
9388
9365
  children: [
9389
9366
  props.beforeItem?.(item, idx),
9390
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
9391
- isClusterItem(item) ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !props.renderClusterCard ? /* @__PURE__ */ jsxRuntime.jsx(
9392
- ClusterCardImpl,
9393
- {
9394
- cluster: item,
9395
- labels: props.clusterCardLabels,
9396
- stockLabels: props.stockLabels
9397
- }
9398
- ) : null }) : null,
9399
- !isClusterItem(item) ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !props.renderProductCard ? /* @__PURE__ */ jsxRuntime.jsx(
9400
- ProductCardImpl,
9401
- {
9402
- product: item,
9403
- allowAddToCart: showAddToCart(),
9404
- labels: props.productCardLabels,
9405
- stockLabels: props.stockLabels,
9406
- priceLabels: props.priceLabels,
9407
- addToCartLabels: props.addToCartLabels
9408
- }
9409
- ) : null }) : null
9410
- ] }),
9367
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: isClusterItem(item) ? /* @__PURE__ */ jsxRuntime.jsx(
9368
+ ClusterCardImpl,
9369
+ {
9370
+ cluster: item,
9371
+ labels: props.clusterCardLabels,
9372
+ stockLabels: props.stockLabels
9373
+ }
9374
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9375
+ ProductCardImpl,
9376
+ {
9377
+ product: item,
9378
+ allowAddToCart: showAddToCart(),
9379
+ labels: props.productCardLabels,
9380
+ stockLabels: props.stockLabels,
9381
+ priceLabels: props.priceLabels,
9382
+ addToCartLabels: props.addToCartLabels
9383
+ }
9384
+ ) }),
9411
9385
  props.afterItem?.(item, idx)
9412
9386
  ]
9413
9387
  },
@@ -11929,8 +11903,9 @@ function FavoriteListDetails(rawProps) {
11929
11903
  if (contact.contactId) {
11930
11904
  priceInput.contactId = contact.contactId;
11931
11905
  }
11932
- if (contact.company?.companyId) {
11933
- priceInput.companyId = contact.company.companyId;
11906
+ const activeCompanyId = props.companyId ?? contact.company?.companyId;
11907
+ if (activeCompanyId) {
11908
+ priceInput.companyId = activeCompanyId;
11934
11909
  }
11935
11910
  }
11936
11911
  }
@@ -12006,12 +11981,15 @@ function FavoriteListDetails(rawProps) {
12006
11981
  setPrevListId(props.favoriteListId || "");
12007
11982
  fetchList();
12008
11983
  }, []);
11984
+ const prevCompanyIdRef = React6.useRef(props.companyId);
12009
11985
  React6.useEffect(() => {
12010
- if (props.favoriteListId && props.favoriteListId !== prevListId) {
12011
- setPrevListId(props.favoriteListId);
12012
- fetchList();
12013
- }
12014
- }, [props.favoriteListId]);
11986
+ const listChanged = !!props.favoriteListId && props.favoriteListId !== prevListId;
11987
+ const companyChanged = props.companyId !== prevCompanyIdRef.current;
11988
+ if (!listChanged && !companyChanged) return;
11989
+ if (listChanged) setPrevListId(props.favoriteListId);
11990
+ prevCompanyIdRef.current = props.companyId;
11991
+ fetchList();
11992
+ }, [props.favoriteListId, props.companyId]);
12015
11993
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(`propeller-favorite-list-details ${props.className || ""}`), "data-loading": loading ? "true" : "false", children: [
12016
11994
  loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-favorite-list-details__skeleton space-y-4", children: [1, 2, 3]?.map((i) => /* @__PURE__ */ jsxRuntime.jsxs(
12017
11995
  "div",
@@ -13887,7 +13865,7 @@ function OrderList(rawProps) {
13887
13865
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(`propeller-order-list ${props.className || ""}`), "data-loading": loading ? "true" : "false", children: [
13888
13866
  props.enableSearch && searchFields().length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-order-list__filters mb-6 bg-card p-4 rounded-container shadow space-y-4", children: [
13889
13867
  searchFields().includes("term") ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-order-list__search-field w-full", children: [
13890
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "propeller-order-list__filter-label block text-sm font-medium text-muted-foreground capitalize mb-1", children: getColumnLabel("term") }),
13868
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "propeller-order-list__filter-label block text-sm font-medium text-muted-foreground mb-1", children: getColumnLabel("term") }),
13891
13869
  /* @__PURE__ */ jsxRuntime.jsx(
13892
13870
  "input",
13893
13871
  {
@@ -13915,7 +13893,7 @@ function OrderList(rawProps) {
13915
13893
  )
13916
13894
  ] }) : null,
13917
13895
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4", children: searchFields().filter((f) => f !== "term")?.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
13918
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "propeller-order-list__filter-label block text-sm font-medium text-muted-foreground capitalize", children: getColumnLabel(field) }),
13896
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "propeller-order-list__filter-label block text-sm font-medium text-muted-foreground", children: getColumnLabel(field) }),
13919
13897
  field === "createdAt" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex space-x-2 w-full", children: [
13920
13898
  /* @__PURE__ */ jsxRuntime.jsx(
13921
13899
  "input",
@@ -15619,7 +15597,7 @@ function ProductDescription(props) {
15619
15597
  !shouldTruncate || expanded ? /* @__PURE__ */ jsxRuntime.jsx(
15620
15598
  "div",
15621
15599
  {
15622
- className: "propeller-product-description__content prose prose-slate max-w-none text-muted-foreground",
15600
+ className: "propeller-product-description__content text-muted-foreground",
15623
15601
  dangerouslySetInnerHTML: { __html: html }
15624
15602
  }
15625
15603
  ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "propeller-product-description__truncated text-muted-foreground", children: getTruncated() }),
@@ -16361,7 +16339,7 @@ function ProductShortDescription(props) {
16361
16339
  "div",
16362
16340
  {
16363
16341
  dangerouslySetInnerHTML: { __html: html },
16364
- className: cn(`propeller-product-short-description prose prose-slate max-w-none text-muted-foreground ${props.className || ""}`)
16342
+ className: cn(`propeller-product-short-description text-muted-foreground ${props.className || ""}`)
16365
16343
  }
16366
16344
  ) : null });
16367
16345
  }