@propeller-commerce/propeller-v2-react-ui 0.16.0 → 0.19.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/dist/pure.d.cts CHANGED
@@ -252,9 +252,22 @@ interface OrderItemCardProps {
252
252
  * This component is RSC-safe (no hooks, no context), so the host has to pass
253
253
  * it — there is nothing to read it from. Without it a non-default storefront
254
254
  * renders default-language names and, worse, default-language slugs in the
255
- * links (PWP-940).
255
+ * links.
256
256
  */
257
257
  language?: string;
258
+ /**
259
+ * URL builders from the host's configuration. Supply them and the item link
260
+ * is generated the same way every other link on the storefront is — which is
261
+ * what carries the locale prefix. Without it the component falls back to a
262
+ * bare `/product/:id/:slug`, and on a prefixed storefront that link drops the
263
+ * visitor onto the default language.
264
+ */
265
+ configuration?: {
266
+ urls?: {
267
+ getProductUrl?: (product: Product, language?: string) => string;
268
+ getClusterUrl?: (cluster: Cluster, language?: string) => string;
269
+ };
270
+ };
258
271
  /** Should the item title be a link to the PDP */
259
272
  titleLinkable?: boolean;
260
273
  /** Display a small thumbnail of the order item */
@@ -297,6 +310,12 @@ declare function OrderItemCard(props: OrderItemCardProps): React.JSX.Element;
297
310
  */
298
311
 
299
312
  interface OrderSummaryProps {
313
+ /**
314
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
315
+ * rendered in — without it they fall back to Dutch separators regardless of
316
+ * the language the shopper is reading.
317
+ */
318
+ language?: string;
300
319
  /** The order object from propeller-sdk-v2 */
301
320
  order: any;
302
321
  /** The CSS class for the order summary container */
@@ -360,6 +379,12 @@ declare function OrderSummary(props: OrderSummaryProps): React.JSX.Element;
360
379
  */
361
380
 
362
381
  interface OrderTotalsProps {
382
+ /**
383
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
384
+ * rendered in — without it they fall back to Dutch separators regardless of
385
+ * the language the shopper is reading.
386
+ */
387
+ language?: string;
363
388
  /** The order/quote used to populate the summary data */
364
389
  order: Order;
365
390
  /** Order summary block title */
@@ -397,6 +422,12 @@ declare function OrderTotals(props: OrderTotalsProps): React.JSX.Element;
397
422
  */
398
423
 
399
424
  interface ProductBulkPricesProps {
425
+ /**
426
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
427
+ * rendered in — without it they fall back to Dutch separators regardless of
428
+ * the language the shopper is reading.
429
+ */
430
+ language?: string;
400
431
  /**
401
432
  * Bulk price tiers from the product.
402
433
  * Obtain from `product.bulkPrices`.
@@ -473,6 +504,12 @@ declare function ProductDownloads(props: ProductDownloadsProps): React.JSX.Eleme
473
504
  */
474
505
 
475
506
  interface ProductPriceProps {
507
+ /**
508
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
509
+ * rendered in — without it they fall back to Dutch separators regardless of
510
+ * the language the shopper is reading.
511
+ */
512
+ language?: string;
476
513
  /**
477
514
  * ProductPrice object from the product.
478
515
  * Obtain from `product.price`.
package/dist/pure.d.ts CHANGED
@@ -252,9 +252,22 @@ interface OrderItemCardProps {
252
252
  * This component is RSC-safe (no hooks, no context), so the host has to pass
253
253
  * it — there is nothing to read it from. Without it a non-default storefront
254
254
  * renders default-language names and, worse, default-language slugs in the
255
- * links (PWP-940).
255
+ * links.
256
256
  */
257
257
  language?: string;
258
+ /**
259
+ * URL builders from the host's configuration. Supply them and the item link
260
+ * is generated the same way every other link on the storefront is — which is
261
+ * what carries the locale prefix. Without it the component falls back to a
262
+ * bare `/product/:id/:slug`, and on a prefixed storefront that link drops the
263
+ * visitor onto the default language.
264
+ */
265
+ configuration?: {
266
+ urls?: {
267
+ getProductUrl?: (product: Product, language?: string) => string;
268
+ getClusterUrl?: (cluster: Cluster, language?: string) => string;
269
+ };
270
+ };
258
271
  /** Should the item title be a link to the PDP */
259
272
  titleLinkable?: boolean;
260
273
  /** Display a small thumbnail of the order item */
@@ -297,6 +310,12 @@ declare function OrderItemCard(props: OrderItemCardProps): React.JSX.Element;
297
310
  */
298
311
 
299
312
  interface OrderSummaryProps {
313
+ /**
314
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
315
+ * rendered in — without it they fall back to Dutch separators regardless of
316
+ * the language the shopper is reading.
317
+ */
318
+ language?: string;
300
319
  /** The order object from propeller-sdk-v2 */
301
320
  order: any;
302
321
  /** The CSS class for the order summary container */
@@ -360,6 +379,12 @@ declare function OrderSummary(props: OrderSummaryProps): React.JSX.Element;
360
379
  */
361
380
 
362
381
  interface OrderTotalsProps {
382
+ /**
383
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
384
+ * rendered in — without it they fall back to Dutch separators regardless of
385
+ * the language the shopper is reading.
386
+ */
387
+ language?: string;
363
388
  /** The order/quote used to populate the summary data */
364
389
  order: Order;
365
390
  /** Order summary block title */
@@ -397,6 +422,12 @@ declare function OrderTotals(props: OrderTotalsProps): React.JSX.Element;
397
422
  */
398
423
 
399
424
  interface ProductBulkPricesProps {
425
+ /**
426
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
427
+ * rendered in — without it they fall back to Dutch separators regardless of
428
+ * the language the shopper is reading.
429
+ */
430
+ language?: string;
400
431
  /**
401
432
  * Bulk price tiers from the product.
402
433
  * Obtain from `product.bulkPrices`.
@@ -473,6 +504,12 @@ declare function ProductDownloads(props: ProductDownloadsProps): React.JSX.Eleme
473
504
  */
474
505
 
475
506
  interface ProductPriceProps {
507
+ /**
508
+ * Storefront language ('EN', 'NL', …). Decides the number format prices are
509
+ * rendered in — without it they fall back to Dutch separators regardless of
510
+ * the language the shopper is reading.
511
+ */
512
+ language?: string;
476
513
  /**
477
514
  * ProductPrice object from the product.
478
515
  * Obtain from `product.price`.
package/dist/pure.js CHANGED
@@ -1,9 +1,14 @@
1
- import { buildClusterJsonLd, safeJsonStringify, buildItemListJsonLd, buildProductJsonLd, getLabel, getLocalizedValue, isContentHidden, formatPrice, getCountryName } from '@propeller-commerce/propeller-v2-core-ui';
1
+ import { buildClusterJsonLd, safeJsonStringify, buildItemListJsonLd, buildProductJsonLd, getLabel, getLocalizedValue, isContentHidden, formatPrice, localeForLanguage, getCountryName } from '@propeller-commerce/propeller-v2-core-ui';
2
+ import { clsx } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
2
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
5
  import * as React from 'react';
4
6
  import { OrderDiscountType, YesNo } from '@propeller-commerce/propeller-sdk-v2';
5
7
 
6
8
  // src/components/Breadcrumbs.tsx
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
7
12
  function Breadcrumbs(props) {
8
13
  const lang = props.language || "NL";
9
14
  const path = props.categoryPath || [];
@@ -36,7 +41,7 @@ function Breadcrumbs(props) {
36
41
  return index === displayItems.length - 1;
37
42
  }
38
43
  const showSeparatorBefore = (index) => props.showHome !== false || index > 0;
39
- return /* @__PURE__ */ jsx("nav", { "aria-label": getLabel(props.labels, "breadcrumbAriaLabel", "Breadcrumb"), className: `propeller-breadcrumbs ${props.className || ""}`, children: /* @__PURE__ */ jsxs("ol", { className: "propeller-breadcrumbs__list flex flex-wrap items-center text-sm text-muted-foreground", children: [
44
+ return /* @__PURE__ */ jsx("nav", { "aria-label": getLabel(props.labels, "breadcrumbAriaLabel", "Breadcrumb"), className: cn(`propeller-breadcrumbs ${props.className || ""}`), children: /* @__PURE__ */ jsxs("ol", { className: "propeller-breadcrumbs__list flex flex-wrap items-center text-sm text-muted-foreground", children: [
40
45
  props.showHome !== false ? /* @__PURE__ */ jsx("li", { className: "propeller-breadcrumbs__item flex items-center", "data-home": "true", children: /* @__PURE__ */ jsx(
41
46
  "a",
42
47
  {
@@ -142,7 +147,7 @@ function CategoryShortDescription(props) {
142
147
  (d) => d.language === props.language
143
148
  );
144
149
  const html = match?.value || "";
145
- return /* @__PURE__ */ jsx(Fragment, { children: !!html ? /* @__PURE__ */ jsx("div", { className: `propeller-category-short-description mb-6 ${props.className || ""}`, children: /* @__PURE__ */ jsx(
150
+ return /* @__PURE__ */ jsx(Fragment, { children: !!html ? /* @__PURE__ */ jsx("div", { className: cn(`propeller-category-short-description mb-6 ${props.className || ""}`), children: /* @__PURE__ */ jsx(
146
151
  "div",
147
152
  {
148
153
  className: "propeller-category-short-description__content prose prose-slate max-w-none text-muted-foreground",
@@ -152,7 +157,7 @@ function CategoryShortDescription(props) {
152
157
  }
153
158
  var CategoryShortDescription_default = CategoryShortDescription;
154
159
  function GridTitle(props) {
155
- return /* @__PURE__ */ jsx("div", { className: `propeller-grid-title mb-8 ${props.className || ""}`, children: /* @__PURE__ */ jsxs("div", { className: "propeller-grid-title__row flex items-baseline gap-3 mb-3", children: [
160
+ return /* @__PURE__ */ jsx("div", { className: cn(`propeller-grid-title mb-8 ${props.className || ""}`), children: /* @__PURE__ */ jsxs("div", { className: "propeller-grid-title__row flex items-baseline gap-3 mb-3", children: [
156
161
  props.headingLevel === "h2" ? /* @__PURE__ */ jsx("h2", { className: "propeller-grid-title__heading text-3xl sm:text-4xl font-bold tracking-tight", children: props.title }) : null,
157
162
  props.headingLevel !== "h2" ? /* @__PURE__ */ jsx("h1", { className: "propeller-grid-title__heading text-3xl sm:text-4xl font-bold tracking-tight", children: props.title }) : null
158
163
  ] }) });
@@ -200,7 +205,7 @@ function ItemStock(props) {
200
205
  return /* @__PURE__ */ jsx(Fragment, { children: hasInventory() ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
201
206
  "div",
202
207
  {
203
- className: `propeller-item-stock flex flex-wrap items-center gap-1.5 ${props.className || ""}`,
208
+ className: cn(`propeller-item-stock flex flex-wrap items-center gap-1.5 ${props.className || ""}`),
204
209
  "data-available": isAvailable() ? "true" : "false",
205
210
  "data-stock": getStockStatusData(),
206
211
  children: [
@@ -242,20 +247,21 @@ var ItemStock_default = React.memo(ItemStock);
242
247
  function getProductName(orderItem, language) {
243
248
  return getLocalizedValue(orderItem?.product?.names, language, "") || orderItem?.name || "Unknown Product";
244
249
  }
245
- function getClusterUrl(orderItem, language) {
250
+ function getClusterUrl(orderItem, language, urls) {
246
251
  const cluster = orderItem?.product?.cluster;
247
252
  if (!cluster) return "";
248
253
  const id = cluster.clusterId;
249
254
  const slug = getLocalizedValue(cluster.slugs, language, "");
250
255
  if (!id || !slug) return "";
251
- return `/cluster/${id}/${slug}`;
256
+ return urls?.getClusterUrl?.(cluster, language) || `/cluster/${id}/${slug}`;
252
257
  }
253
- function getProductUrl(orderItem, language) {
254
- const cu = getClusterUrl(orderItem, language);
258
+ function getProductUrl(orderItem, language, urls) {
259
+ const cu = getClusterUrl(orderItem, language, urls);
255
260
  if (cu) return cu;
256
- const id = orderItem?.product?.productId;
257
- const slug = getLocalizedValue(orderItem?.product?.slugs, language, "");
258
- if (id && slug) return `/product/${id}/${slug}`;
261
+ const product = orderItem?.product;
262
+ const id = product?.productId;
263
+ const slug = getLocalizedValue(product?.slugs, language, "");
264
+ if (id && slug) return urls?.getProductUrl?.(product, language) || `/product/${id}/${slug}`;
259
265
  return "";
260
266
  }
261
267
  function OrderItemCard(props) {
@@ -272,7 +278,7 @@ function OrderItemCard(props) {
272
278
  const productName = getProductName(item, props.language);
273
279
  const productSku = item?.product?.sku || item?.sku || "";
274
280
  const productImage = item?.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
275
- const productUrl = getProductUrl(item, props.language);
281
+ const productUrl = getProductUrl(item, props.language, props.configuration?.urls);
276
282
  const quantity = item?.quantity || 0;
277
283
  const priceTotal = item?.priceTotal || 0;
278
284
  const discount = item?.discount || 0;
@@ -285,7 +291,7 @@ function OrderItemCard(props) {
285
291
  return props.formatPrice(price);
286
292
  }
287
293
  if (!price && price !== 0) return "-";
288
- return formatPrice(price, { symbol: props.currency ?? "\u20AC" });
294
+ return formatPrice(price, { symbol: props.currency ?? "\u20AC", locale: localeForLanguage(props.language) });
289
295
  }
290
296
  const discountDisplay = discountPercentage > 0 ? `${formatItemPrice(discount)} (${discountPercentage.toFixed(2).replace(".", ",")}%)` : formatItemPrice(discount);
291
297
  return /* @__PURE__ */ jsxs("tbody", { className: "propeller-order-item-card", children: [
@@ -426,7 +432,7 @@ function OrderSummary(props) {
426
432
  if (props.formatPrice) {
427
433
  return props.formatPrice(price);
428
434
  }
429
- return formatPrice(price || 0, { symbol: props.currency ?? "\u20AC" });
435
+ return formatPrice(price || 0, { symbol: props.currency ?? "\u20AC", locale: localeForLanguage(props.language) });
430
436
  }
431
437
  function numericDate(dateString) {
432
438
  const d = new Date(dateString);
@@ -534,7 +540,7 @@ function OrderTotals(props) {
534
540
  const postageData = props.order?.postageData;
535
541
  function formatItemPrice(price) {
536
542
  if (props.formatPrice) return props.formatPrice(price);
537
- return formatPrice(price || 0, { symbol: props.currency ?? "\u20AC" });
543
+ return formatPrice(price || 0, { symbol: props.currency ?? "\u20AC", locale: localeForLanguage(props.language) });
538
544
  }
539
545
  const subtotal = total?.gross || 0;
540
546
  const hasDiscount = !!total && total.discountType !== OrderDiscountType.N && total.discountValue > 0;
@@ -662,7 +668,7 @@ function ProductBulkPrices(props) {
662
668
  function getPrice(tier) {
663
669
  const value = includeTax ? tier.net : tier.gross;
664
670
  if (value === null || value === void 0) return "";
665
- return formatPrice(value, { symbol: props.currency ?? "\u20AC" });
671
+ return formatPrice(value, { symbol: props.currency ?? "\u20AC", locale: localeForLanguage(props.language) });
666
672
  }
667
673
  function getQuantityLabel(tier, index) {
668
674
  const discount = tier.discount;
@@ -673,13 +679,14 @@ function ProductBulkPrices(props) {
673
679
  if (nextQty) return `${qty}\u2013${nextQty - 1}`;
674
680
  return `${qty}+`;
675
681
  }
682
+ const title = props.labels?.title ?? "Volume pricing";
676
683
  return /* @__PURE__ */ jsx(Fragment, { children: !isHidden && hasItems ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
677
684
  "div",
678
685
  {
679
- className: `propeller-product-bulk-prices ${props.className || ""}`,
686
+ className: cn(`propeller-product-bulk-prices ${props.className || ""}`),
680
687
  "data-include-tax": includeTax ? "true" : "false",
681
688
  children: [
682
- getLabel(props.labels, "title", "Volume pricing") ? /* @__PURE__ */ jsx("h3", { className: "propeller-product-bulk-prices__title text-base font-semibold text-foreground mb-3", children: getLabel(props.labels, "title", "Volume pricing") }) : null,
689
+ title ? /* @__PURE__ */ jsx("h3", { className: "propeller-product-bulk-prices__title text-base font-semibold text-foreground mb-3", children: title }) : null,
683
690
  /* @__PURE__ */ jsx("div", { className: "propeller-product-bulk-prices__table-wrapper overflow-hidden rounded-container border border-border", children: /* @__PURE__ */ jsxs("table", { className: "propeller-product-bulk-prices__table w-full text-sm", children: [
684
691
  /* @__PURE__ */ jsx("thead", { className: "propeller-product-bulk-prices__thead bg-muted/50", children: /* @__PURE__ */ jsxs("tr", { children: [
685
692
  /* @__PURE__ */ jsx("th", { className: "propeller-product-bulk-prices__th propeller-product-bulk-prices__th--quantity px-4 py-2 text-left font-medium text-muted-foreground", children: getLabel(props.labels, "quantityFrom", "Qty from") }),
@@ -716,7 +723,7 @@ function ProductDownloads(props) {
716
723
  const match = alts.find((a) => a.language === lang);
717
724
  return match?.value || alts?.[0]?.value || "Download";
718
725
  }
719
- return /* @__PURE__ */ jsxs("div", { className: `propeller-product-downloads ${props.className || ""}`, children: [
726
+ return /* @__PURE__ */ jsxs("div", { className: cn(`propeller-product-downloads ${props.className || ""}`), children: [
720
727
  hasItems ? /* @__PURE__ */ jsx("h3", { className: "propeller-product-downloads__title text-base font-semibold text-foreground mb-3", children: getLabel(props.labels, "title", "Downloads") }) : null,
721
728
  hasItems ? /* @__PURE__ */ jsx("ul", { className: "propeller-product-downloads__list space-y-2", children: items.map((doc, index) => /* @__PURE__ */ jsx("li", { className: "propeller-product-downloads__item", children: !!getDocumentUrl(doc) ? /* @__PURE__ */ jsxs(
722
729
  "a",
@@ -776,7 +783,7 @@ function ProductPriceDisplay(props) {
776
783
  }
777
784
  function formatPrice5(value) {
778
785
  if (value === null || value === void 0) return "";
779
- return formatPrice(value, { symbol: props.currency ?? "\u20AC" });
786
+ return formatPrice(value, { symbol: props.currency ?? "\u20AC", locale: localeForLanguage(props.language) });
780
787
  }
781
788
  function getOptionsTotal(useNet) {
782
789
  const options = props.options || [];
@@ -819,7 +826,7 @@ function ProductPriceDisplay(props) {
819
826
  function getSecondaryTaxLabel() {
820
827
  return props.includeTax ? getLabel(props.labels, "exclTax", "excl. VAT") : getLabel(props.labels, "inclTax", "incl. VAT");
821
828
  }
822
- return /* @__PURE__ */ jsxs("div", { className: `propeller-product-price ${props.className || ""}`, "data-hidden": isHidden() ? "true" : "false", children: [
829
+ return /* @__PURE__ */ jsxs("div", { className: cn(`propeller-product-price ${props.className || ""}`), "data-hidden": isHidden() ? "true" : "false", children: [
823
830
  isHidden() && props.showLoginPrompt !== false ? /* @__PURE__ */ jsx("p", { className: "propeller-product-price__login-prompt text-sm text-muted-foreground italic", children: getLabel(props.labels, "loginToSeePrices", "Log in to see prices") }) : null,
824
831
  !isHidden() && !!getLeadingPrice() ? /* @__PURE__ */ jsxs("div", { className: "propeller-product-price__content flex flex-col gap-0.5", children: [
825
832
  /* @__PURE__ */ jsxs("div", { className: "propeller-product-price__primary flex items-baseline gap-2", children: [
@@ -853,7 +860,7 @@ function ProductShortDescription(props) {
853
860
  "div",
854
861
  {
855
862
  dangerouslySetInnerHTML: { __html: html },
856
- className: `propeller-product-short-description prose prose-slate max-w-none text-muted-foreground ${props.className || ""}`
863
+ className: cn(`propeller-product-short-description prose prose-slate max-w-none text-muted-foreground ${props.className || ""}`)
857
864
  }
858
865
  ) : null });
859
866
  }
@@ -896,7 +903,7 @@ function ProductVideos(props) {
896
903
  }
897
904
  return uri;
898
905
  }
899
- return /* @__PURE__ */ jsxs("div", { className: `propeller-product-videos ${props.className || ""}`, children: [
906
+ return /* @__PURE__ */ jsxs("div", { className: cn(`propeller-product-videos ${props.className || ""}`), children: [
900
907
  hasItems ? /* @__PURE__ */ jsx("h3", { className: "propeller-product-videos__title text-base font-semibold text-foreground mb-3", children: getLabel(props.labels, "title", "Videos") }) : null,
901
908
  hasItems ? /* @__PURE__ */ jsx("div", { className: "propeller-product-videos__list space-y-4", children: items.map((video, index) => /* @__PURE__ */ jsx(
902
909
  "div",