@propeller-commerce/propeller-v2-react-ui 0.11.3 → 0.12.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,59 @@ 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.12.0] - 2026-07-31
12
+
13
+ ### Added
14
+
15
+ - **`productTrackAttributes` on `<ProductGrid>`, `<SearchBar>` and
16
+ `useProductSearch`.** Pass the attribute names a card renders — e.g.
17
+ `productTrackAttributes={['MPN']}` — and they arrive on
18
+ `product.attributes` for every item in the result, ready to read from a
19
+ card slot like `belowName`. `useProductInfo` has had an option of this name
20
+ since the PDP work; the grid side simply never forwarded one, so hosts that
21
+ set it in their portal config saw it silently ignored on listings and had
22
+ to fetch attributes themselves in a second request.
23
+
24
+ Requires `propeller-sdk-v2` >= 0.16.0, where the `category` and `products`
25
+ operations gained `$attributeResultSearchInput` and `ProductGridFields`
26
+ gained the matching `attributes(input:)` selection.
27
+
28
+ Naming the attributes you render is not just a payload optimisation — it is
29
+ the correct behaviour. With no input the server returns the *first page* of
30
+ a product's attributes (12 per product), so on a product carrying more than
31
+ 12 the ones on later pages are missing with no error. Filtering to the names
32
+ you need also cut a 24-product category response from ~168 KB to ~124 KB in
33
+ our testing.
34
+
35
+ ## [0.11.4] - 2026-07-31
36
+
37
+ ### Fixed
38
+
39
+ - **The add-to-cart button overflowed its product card in narrow columns.**
40
+ The submit button is `flex-1`, but a flex item defaults to
41
+ `min-width: auto` and so refuses to shrink below its content width — icon
42
+ plus label plus `px-6` of padding. In a slider or a dense grid the button
43
+ ran past the card's edge instead of fitting inside it. Longer non-English
44
+ labels hit this first: Dutch "Toevoegen" is roughly twice the width of the
45
+ "Add" the padding was sized around. The button now carries `min-w-0`, eases
46
+ its padding to `px-3 sm:px-6`, and wraps its label in a `truncate` span, so
47
+ it shrinks to the column and ellipsises rather than overflowing. Same fix
48
+ for `<ClusterCard>`'s "View cluster" link, which had the identical shape.
49
+ - **Hovering a card's CTA lit up the whole card.** The card root is a
50
+ `group` with `hover:shadow-md`, so hovering the button — a descendant —
51
+ raised the card shadow, shifted its border and zoomed the product image at
52
+ the same time as the button's own hover state. Three things answering one
53
+ intent read as the card being clickable rather than the button. While the
54
+ pointer is on a CTA the card-level highlight and the image zoom now stand
55
+ down and the button owns the hover; moving anywhere else on the card is
56
+ unchanged. Browsers without `:has()` keep the previous behaviour.
57
+
58
+ ### Changed
59
+
60
+ - **`<AddToCart>`'s hover shade now matches the other CTAs.** It darkened to
61
+ `bg-secondary/90` where every other solid button in the package uses `/80`.
62
+ Now `/80` as well.
63
+
11
64
  ## [0.11.3] - 2026-07-31
12
65
 
13
66
  ### Fixed
package/dist/index.cjs CHANGED
@@ -2090,7 +2090,7 @@ function useProductInfo(options) {
2090
2090
  if (options.user && "customerId" in options.user) input.customerId = options.user.customerId;
2091
2091
  return input;
2092
2092
  }
2093
- function buildAttributeInput() {
2093
+ function buildAttributeInput2() {
2094
2094
  const names = options.productTrackAttributes;
2095
2095
  if (!names || names.length === 0) return void 0;
2096
2096
  return { attributeDescription: { names } };
@@ -2113,7 +2113,7 @@ function useProductInfo(options) {
2113
2113
  orderlistIds = (orderlists?.items ?? []).map((ol) => ol.id);
2114
2114
  }
2115
2115
  const service = propellerV2CoreUi.createServices(graphqlClient).product;
2116
- const attributeInput = buildAttributeInput();
2116
+ const attributeInput = buildAttributeInput2();
2117
2117
  const variables = {
2118
2118
  productId,
2119
2119
  language,
@@ -2203,6 +2203,10 @@ function useProductInfo(options) {
2203
2203
  };
2204
2204
  }
2205
2205
  var inflightFetches2 = /* @__PURE__ */ new Set();
2206
+ function buildAttributeInput(names) {
2207
+ if (!names || names.length === 0) return void 0;
2208
+ return { attributeDescription: { names } };
2209
+ }
2206
2210
  function useProductSearch(options) {
2207
2211
  const { graphqlClient, configuration } = options;
2208
2212
  const isControlled = options.products !== void 0;
@@ -2335,6 +2339,7 @@ function useProductSearch(options) {
2335
2339
  const filterAvailableAttributeInput = {
2336
2340
  isSearchable: true
2337
2341
  };
2342
+ const attributeInput = buildAttributeInput(options.productTrackAttributes);
2338
2343
  const variables = {
2339
2344
  categoryId: catId,
2340
2345
  language: lang,
@@ -2342,7 +2347,8 @@ function useProductSearch(options) {
2342
2347
  priceCalculateProductInput,
2343
2348
  filterAvailableAttributeInput,
2344
2349
  imageSearchFilters: configuration?.imageSearchFiltersGrid,
2345
- imageVariantFilters: configuration?.imageVariantFiltersMedium
2350
+ imageVariantFilters: configuration?.imageVariantFiltersMedium,
2351
+ ...attributeInput && { attributeResultSearchInput: attributeInput }
2346
2352
  };
2347
2353
  const response = await service.getCategory(variables);
2348
2354
  if (thisId !== fetchIdRef.current) return;
@@ -2403,7 +2409,9 @@ function useProductSearch(options) {
2403
2409
  taxZone,
2404
2410
  pageSize,
2405
2411
  pagination.currentPage,
2406
- configuration
2412
+ configuration,
2413
+ // Stringified: callers pass an array literal, so identity changes each render.
2414
+ JSON.stringify(options.productTrackAttributes ?? [])
2407
2415
  ]);
2408
2416
  const search = React6.useCallback(
2409
2417
  (term) => {
@@ -2482,13 +2490,15 @@ function useProductSearch(options) {
2482
2490
  ...contactId !== void 0 && { contactId },
2483
2491
  ...customerId !== void 0 && { customerId }
2484
2492
  };
2493
+ const attributeInput = buildAttributeInput(options.productTrackAttributes);
2485
2494
  const variables = {
2486
2495
  categoryId: catId,
2487
2496
  language,
2488
2497
  categoryProductSearchInput,
2489
2498
  priceCalculateProductInput,
2490
2499
  imageSearchFilters: configuration?.imageSearchFiltersGrid,
2491
- imageVariantFilters: configuration?.imageVariantFiltersMedium
2500
+ imageVariantFilters: configuration?.imageVariantFiltersMedium,
2501
+ ...attributeInput && { attributeResultSearchInput: attributeInput }
2492
2502
  };
2493
2503
  const response = await service.getCategory(variables);
2494
2504
  const productsResponse = response?.products;
@@ -2507,7 +2517,7 @@ function useProductSearch(options) {
2507
2517
  }, 300);
2508
2518
  },
2509
2519
  // eslint-disable-next-line react-hooks/exhaustive-deps
2510
- [graphqlClient, language, configuration, userKey, JSON.stringify(options.orderlistIds ?? []), options.applyOrderlists]
2520
+ [graphqlClient, language, configuration, userKey, JSON.stringify(options.orderlistIds ?? []), options.applyOrderlists, JSON.stringify(options.productTrackAttributes ?? [])]
2511
2521
  );
2512
2522
  React6.useEffect(() => {
2513
2523
  if (!isControlled) fetchProducts();
@@ -4607,7 +4617,7 @@ function AddToCart(rawProps) {
4607
4617
  "button",
4608
4618
  {
4609
4619
  type: "button",
4610
- className: "propeller-add-to-cart__submit flex-1 basis-full md:basis-auto inline-flex justify-center items-center gap-2 h-10 px-6 border border-transparent text-sm font-medium rounded-control text-secondary-foreground bg-secondary hover:bg-secondary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
4620
+ className: "propeller-add-to-cart__submit flex-1 min-w-0 basis-full md:basis-auto inline-flex justify-center items-center gap-2 h-10 px-3 sm:px-6 border border-transparent text-sm font-medium rounded-control text-secondary-foreground bg-secondary hover:bg-secondary/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
4611
4621
  onClick: () => handleAddToCart(),
4612
4622
  disabled: loading,
4613
4623
  children: [
@@ -4629,8 +4639,7 @@ function AddToCart(rawProps) {
4629
4639
  ]
4630
4640
  }
4631
4641
  ),
4632
- loading ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: propellerV2CoreUi.getLabel(props.labels, "adding", "Adding...") }) : null,
4633
- !loading ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: propellerV2CoreUi.getLabel(props.labels, "add", "Add") }) : null
4642
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-add-to-cart__submit-label min-w-0 truncate", children: loading ? propellerV2CoreUi.getLabel(props.labels, "adding", "Adding...") : propellerV2CoreUi.getLabel(props.labels, "add", "Add") })
4634
4643
  ]
4635
4644
  }
4636
4645
  )
@@ -8383,8 +8392,8 @@ function ClusterCardViewClusterLink(props) {
8383
8392
  {
8384
8393
  href: derived.clusterUrl,
8385
8394
  onClick: handleClusterClick,
8386
- className: props.className ?? "propeller-cluster-card__cta-link flex w-full items-center justify-center rounded-control bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/80 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
8387
- children: props.label ?? derived.viewClusterLabel
8395
+ className: props.className ?? "propeller-cluster-card__cta-link flex w-full min-w-0 items-center justify-center rounded-control bg-primary px-3 sm:px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/80 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
8396
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-cluster-card__cta-label min-w-0 truncate", children: props.label ?? derived.viewClusterLabel })
8388
8397
  }
8389
8398
  );
8390
8399
  }
@@ -9035,6 +9044,7 @@ function ProductGrid(rawProps) {
9035
9044
  brand: props.brand,
9036
9045
  orderlistIds: props.orderlistIds,
9037
9046
  applyOrderlists: props.applyOrderlists,
9047
+ productTrackAttributes: props.productTrackAttributes,
9038
9048
  language: props.language,
9039
9049
  taxZone: props.taxZone,
9040
9050
  user: props.user,
@@ -18397,7 +18407,8 @@ function SearchBar(rawProps) {
18397
18407
  user: props.user,
18398
18408
  companyId: props.companyId,
18399
18409
  orderlistIds: props.orderlistIds,
18400
- applyOrderlists: props.applyOrderlists
18410
+ applyOrderlists: props.applyOrderlists,
18411
+ productTrackAttributes: props.productTrackAttributes
18401
18412
  });
18402
18413
  const results = searchResults.slice(0, maxResults).map((item) => mapToSearchBarResult(item, props.language));
18403
18414
  const itemsFound = searchItemsFound;