@propeller-commerce/propeller-v2-react-ui 0.4.15 → 0.4.17

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,43 @@ 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.4.17] - 2026-07-09
12
+
13
+ ### Added
14
+
15
+ - `ProductCard` / `ProductGrid`: new `belowName?: (product: Product) => ReactNode`
16
+ render prop. Renders arbitrary host-supplied content directly below the
17
+ product name (and above the short description / price) in both the grid and
18
+ row layouts, without forking the card — e.g. package descriptions or custom
19
+ badges. On `ProductGrid` it cascades to every card via `ProductGridConfig`;
20
+ on `ProductCard` it can also be set per-card (explicit prop wins over grid
21
+ context). Also exposed as the `<ProductCard.BelowName>` compound subcomponent.
22
+
23
+ ## [0.4.16] - 2026-07-09
24
+
25
+ ### Added
26
+
27
+ - `ProductGrid`, `SearchBar`, `ProductInfo`: new `orderlistIds?: number[]` and
28
+ `applyOrderlists?: boolean` props to scope the product/search fetch to
29
+ specific orderlists (e.g. a chosen B2B contract). When `orderlistIds` is
30
+ non-empty, orderlists are applied (unless `applyOrderlists` is `false`);
31
+ otherwise `applyOrderlists: false` is sent so an authenticated user without a
32
+ contract still sees the full catalogue. Threaded through `useProductSearch`
33
+ and `useProductInfo` (explicit ids override the composable's default
34
+ all-company-orderlists resolution).
35
+ - `Menu`: new `getUrl?: (category: Category) => string` prop — a custom URL
36
+ builder that overrides `menuLinkFormat` / `configuration.urls.getCategoryUrl`,
37
+ letting hosts inject dynamic query strings (e.g. `?contract=…`). Mirrors the
38
+ existing `Breadcrumbs.getUrl`.
39
+ - `GridToolbar`: new `hidePriceSort?: boolean` prop to hide the price sort
40
+ option entirely (closed B2B portals where prices are "by quotation").
41
+ - `ProductTabs` / `ProductSpecifications`: new
42
+ `specificationsPackageDescription?` (ProductTabs) → `packageDescription?`
43
+ (ProductSpecifications) passthrough, rendering an extra package-description
44
+ string above the specifications table.
45
+
46
+ All additions are additive and backward-compatible.
47
+
11
48
  ## [0.4.15] - 2026-07-08
12
49
 
13
50
  ### Changed
package/dist/index.cjs CHANGED
@@ -2045,7 +2045,13 @@ function useProductInfo(options) {
2045
2045
  setError(null);
2046
2046
  try {
2047
2047
  let orderlistIds = [];
2048
- if (options.user && options.companyId) {
2048
+ let applyOrderlists = true;
2049
+ if (options.orderlistIds && options.orderlistIds.length > 0) {
2050
+ orderlistIds = options.orderlistIds;
2051
+ applyOrderlists = options.applyOrderlists !== false;
2052
+ } else if (options.applyOrderlists === false) {
2053
+ applyOrderlists = false;
2054
+ } else if (options.user && options.companyId) {
2049
2055
  const orderlistService = propellerV2CoreUi.createServices(graphqlClient).orderlist;
2050
2056
  const searchInput = { companyIds: [options.companyId] };
2051
2057
  const orderlists = await orderlistService.getOrderlists(searchInput);
@@ -2056,7 +2062,7 @@ function useProductInfo(options) {
2056
2062
  const variables = {
2057
2063
  productId,
2058
2064
  language,
2059
- applyOrderlists: true,
2065
+ applyOrderlists,
2060
2066
  orderlistIds,
2061
2067
  imageSearchFilters: imageSearchFilters ?? configuration.imageSearchFiltersGrid,
2062
2068
  imageVariantFilters: imageVariantFilters ?? configuration.imageVariantFiltersLarge ?? configuration.imageVariantFiltersSmall,
@@ -2240,6 +2246,10 @@ function useProductSearch(options) {
2240
2246
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2241
2247
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2242
2248
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
2249
+ const orderlistScope = options.orderlistIds && options.orderlistIds.length > 0 ? {
2250
+ applyOrderlists: options.applyOrderlists !== false,
2251
+ orderlistIds: options.orderlistIds
2252
+ } : { applyOrderlists: false };
2243
2253
  const categoryProductSearchInput = {
2244
2254
  language: lang,
2245
2255
  page: pagination.currentPage,
@@ -2257,7 +2267,8 @@ function useProductSearch(options) {
2257
2267
  ...priceFilter && { price: priceFilter },
2258
2268
  ...sortInputs.length && { sortInputs },
2259
2269
  ...options.companyId && { companyId: options.companyId },
2260
- ...userId !== void 0 && { userId }
2270
+ ...userId !== void 0 && { userId },
2271
+ ...orderlistScope
2261
2272
  };
2262
2273
  const priceCalculateProductInput = {
2263
2274
  taxZone,
@@ -7430,7 +7441,7 @@ function CategoryDescription(props) {
7430
7441
  const plain = html.replace(/<[^>]*>/g, "");
7431
7442
  if (plain.length <= maxLen) return html;
7432
7443
  const truncated = plain.substring(0, maxLen);
7433
- return truncated.substring(0, truncated.lastIndexOf(" ")) + "\xE2\u20AC\xA6";
7444
+ return truncated.substring(0, truncated.lastIndexOf(" ")) + "\u2026";
7434
7445
  }
7435
7446
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !!html ? /* @__PURE__ */ jsxRuntime.jsxs(
7436
7447
  "div",
@@ -10697,7 +10708,8 @@ function GridToolbar(rawProps) {
10697
10708
  }
10698
10709
  function getSortOptions() {
10699
10710
  const opts = props.sortOptions || [];
10700
- return opts.length > 0 ? opts : ALL_SORT_FIELDS;
10711
+ const base = opts.length > 0 ? opts : ALL_SORT_FIELDS;
10712
+ return props.hidePriceSort ? base.filter((f) => f !== propellerSdkV2.ProductSortField.PRICE) : base;
10701
10713
  }
10702
10714
  function getOffsetOptions() {
10703
10715
  const opts = props.offset || [];
@@ -11120,10 +11132,12 @@ function Menu(rawProps) {
11120
11132
  }
11121
11133
  function getCategoryUrl(cat) {
11122
11134
  const lang = props.language || "NL";
11123
- return props.configuration.urls.getCategoryUrl(
11124
- { categoryId: cat.categoryId, slug: [{ value: cat.slug, language: lang }] },
11125
- lang
11126
- );
11135
+ const category = {
11136
+ categoryId: cat.categoryId,
11137
+ slug: [{ value: cat.slug, language: lang }]
11138
+ };
11139
+ if (props.getUrl) return props.getUrl(category);
11140
+ return props.configuration.urls.getCategoryUrl(category, lang);
11127
11141
  }
11128
11142
  function getSubCategories(cat) {
11129
11143
  return (cat.children || []).filter((sub) => sub.name && sub.slug);
@@ -13308,7 +13322,8 @@ var RESOLVE_SPEC2 = {
13308
13322
  addToCartComponent: { grid: "addToCartComponent" },
13309
13323
  imageComponent: { grid: "imageComponent" },
13310
13324
  badgesComponent: { grid: "badgesComponent" },
13311
- favoriteComponent: { grid: "favoriteComponent" }
13325
+ favoriteComponent: { grid: "favoriteComponent" },
13326
+ belowName: { grid: "belowName" }
13312
13327
  };
13313
13328
  var ProductCardContext = React6.createContext(null);
13314
13329
  function useProductCard() {
@@ -13389,6 +13404,7 @@ function ProductCardRoot(rawProps) {
13389
13404
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-product-card__body flex flex-1 flex-row items-center gap-4 px-4 py-2 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5 flex-1 min-w-0", children: [
13390
13405
  props.showSku !== false && sku ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardSku, {}) : null,
13391
13406
  props.showName !== false ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardName, { linkable: true }) : null,
13407
+ /* @__PURE__ */ jsxRuntime.jsx(ProductCardBelowName, {}),
13392
13408
  textLabelValues.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardTextLabels, {}) : null,
13393
13409
  props.showManufacturer && manufacturer ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardManufacturer, {}) : null,
13394
13410
  props.showShortDescription && shortDescription ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardShortDescription, {}) : null
@@ -13404,6 +13420,7 @@ function ProductCardRoot(rawProps) {
13404
13420
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-product-card__body flex flex-1 flex-col gap-1.5 p-3 sm:gap-2 sm:p-4", children: [
13405
13421
  props.showSku !== false && sku ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardSku, {}) : null,
13406
13422
  props.showName !== false ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardName, { linkable: true }) : null,
13423
+ /* @__PURE__ */ jsxRuntime.jsx(ProductCardBelowName, {}),
13407
13424
  textLabelValues.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardTextLabels, {}) : null,
13408
13425
  props.showStock && !!product.inventory ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:block", children: /* @__PURE__ */ jsxRuntime.jsx(ProductCardStock, {}) }) : null,
13409
13426
  props.showManufacturer && manufacturer ? /* @__PURE__ */ jsxRuntime.jsx(ProductCardManufacturer, {}) : null,
@@ -13586,6 +13603,13 @@ function ProductCardStock(props) {
13586
13603
  }
13587
13604
  );
13588
13605
  }
13606
+ function ProductCardBelowName() {
13607
+ const { product, resolved } = useProductCard();
13608
+ if (!resolved.belowName) return null;
13609
+ const content = resolved.belowName(product);
13610
+ if (content == null || content === false) return null;
13611
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-product-card__below-name", children: content });
13612
+ }
13589
13613
  function ProductCardPrice(props) {
13590
13614
  const { product, derived, resolved } = useProductCard();
13591
13615
  if (!product?.price) return null;
@@ -13664,6 +13688,7 @@ ProductCard.Badges = ProductCardBadges;
13664
13688
  ProductCard.Favorite = ProductCardFavorite;
13665
13689
  ProductCard.Name = ProductCardName;
13666
13690
  ProductCard.Sku = ProductCardSku;
13691
+ ProductCard.BelowName = ProductCardBelowName;
13667
13692
  ProductCard.ShortDescription = ProductCardShortDescription;
13668
13693
  ProductCard.Manufacturer = ProductCardManufacturer;
13669
13694
  ProductCard.TextLabels = ProductCardTextLabels;
@@ -13938,7 +13963,7 @@ function useProductGrid() {
13938
13963
  const ctx = React6.useContext(ProductGridSearchContext);
13939
13964
  if (!ctx) {
13940
13965
  throw new Error(
13941
- "<ProductGrid.X> must be rendered inside <ProductGrid>. Got null context \xE2\u20AC\u201D wrap the subcomponents in <ProductGrid categoryId={...}>...</ProductGrid>."
13966
+ "<ProductGrid.X> must be rendered inside <ProductGrid>. Got null context \u2014 wrap the subcomponents in <ProductGrid categoryId={...}>...</ProductGrid>."
13942
13967
  );
13943
13968
  }
13944
13969
  return ctx;
@@ -13959,6 +13984,8 @@ function ProductGrid(rawProps) {
13959
13984
  categoryId: props.categoryId,
13960
13985
  term: props.term,
13961
13986
  brand: props.brand,
13987
+ orderlistIds: props.orderlistIds,
13988
+ applyOrderlists: props.applyOrderlists,
13962
13989
  language: props.language,
13963
13990
  taxZone: props.taxZone,
13964
13991
  user: props.user,
@@ -14044,7 +14071,8 @@ function ProductGrid(rawProps) {
14044
14071
  badgesComponent: props.badgesComponent,
14045
14072
  favoriteComponent: props.favoriteComponent,
14046
14073
  productCardComponent: props.productCardComponent,
14047
- clusterCardComponent: props.clusterCardComponent
14074
+ clusterCardComponent: props.clusterCardComponent,
14075
+ belowName: props.belowName
14048
14076
  };
14049
14077
  if (rawProps.children !== void 0) {
14050
14078
  const compoundCtx = {
@@ -14361,9 +14389,15 @@ function useProductInfoContext() {
14361
14389
  }
14362
14390
  function ProductInfo(rawProps) {
14363
14391
  const props = useInfraProps(rawProps);
14364
- const { product: fetchedProduct, loading, fetchProduct } = useProductInfo(
14365
- props.graphqlClient ? { graphqlClient: props.graphqlClient, language: props.language, configuration: props.configuration } : { graphqlClient: {}, language: props.language, configuration: props.configuration }
14366
- );
14392
+ const { product: fetchedProduct, loading, fetchProduct } = useProductInfo({
14393
+ graphqlClient: props.graphqlClient ?? {},
14394
+ language: props.language,
14395
+ configuration: props.configuration,
14396
+ user: props.user,
14397
+ companyId: props.companyId,
14398
+ orderlistIds: props.orderlistIds,
14399
+ applyOrderlists: props.applyOrderlists
14400
+ });
14367
14401
  const activeProduct = props.product || fetchedProduct;
14368
14402
  function getProductName5() {
14369
14403
  if (!activeProduct) return "";
@@ -15081,13 +15115,14 @@ function ProductSpecifications(rawProps) {
15081
15115
  if (!props.productId || !props.graphqlClient) return;
15082
15116
  fetchSpecs(props.productId);
15083
15117
  }, [props.productId]);
15084
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !loading && hasPublicAttributes() ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(
15118
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: !loading && (hasPublicAttributes() || props.packageDescription) ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(
15085
15119
  "div",
15086
15120
  {
15087
15121
  className: `propeller-product-specifications ${props.className || ""}`,
15088
15122
  "data-layout": props.layout === "list" ? "list" : "table",
15089
15123
  "data-grouped": props.grouping ? "true" : "false",
15090
15124
  children: [
15125
+ props.packageDescription ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "propeller-product-specifications__package-description mb-4 text-sm text-muted-foreground", children: props.packageDescription }) : null,
15091
15126
  !props.grouping ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15092
15127
  props.layout !== "list" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-border", children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "w-full text-sm", children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-border", children: getAttributes()?.map((attr, i) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "propeller-product-specifications__row odd:bg-card even:bg-muted/20", children: [
15093
15128
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-2 font-medium text-foreground w-1/2", children: getAttributeLabel(attr) }),
@@ -15316,7 +15351,8 @@ function ProductTabs(rawProps) {
15316
15351
  attributes: getSpecsAttributes(),
15317
15352
  language: props.language,
15318
15353
  layout: props.specificationsLayout,
15319
- grouping: props.specificationsGrouping
15354
+ grouping: props.specificationsGrouping,
15355
+ packageDescription: props.specificationsPackageDescription
15320
15356
  }
15321
15357
  ) }) : null,
15322
15358
  isActive("downloads") && isTabVisible("downloads") ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -15419,7 +15455,8 @@ function ProductTabs(rawProps) {
15419
15455
  attributes: getSpecsAttributes(),
15420
15456
  language: props.language,
15421
15457
  layout: props.specificationsLayout,
15422
- grouping: props.specificationsGrouping
15458
+ grouping: props.specificationsGrouping,
15459
+ packageDescription: props.specificationsPackageDescription
15423
15460
  }
15424
15461
  ) }) : null
15425
15462
  ] }) : null,
@@ -16206,7 +16243,7 @@ function RegisterForm(rawProps) {
16206
16243
  const contactLabel = props.labels?.contactLabel || "Company";
16207
16244
  const customerLabel = props.labels?.customerLabel || "Consumer";
16208
16245
  const emailPlaceholder = props.labels?.emailPlaceholder || "name@example.com";
16209
- const passwordPlaceholder = props.labels?.passwordPlaceholder || "\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2\xE2\u20AC\xA2";
16246
+ const passwordPlaceholder = props.labels?.passwordPlaceholder || "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
16210
16247
  const passwordMismatchText = props.labels?.passwordMismatch || "Passwords do not match";
16211
16248
  const loginText = props.labels?.loginText || "Already have an account?";
16212
16249
  const loginLinkText = props.labels?.loginLink || "Log in";
@@ -17110,7 +17147,9 @@ function SearchBar(rawProps) {
17110
17147
  graphqlClient: props.graphqlClient,
17111
17148
  language: props.language,
17112
17149
  configuration: props.configuration || {},
17113
- companyId: props.companyId
17150
+ companyId: props.companyId,
17151
+ orderlistIds: props.orderlistIds,
17152
+ applyOrderlists: props.applyOrderlists
17114
17153
  });
17115
17154
  const results = searchResults.slice(0, maxResults).map((item) => mapToSearchBarResult(item));
17116
17155
  const itemsFound = searchItemsFound;