@propeller-commerce/propeller-v2-react-ui 0.15.4 → 0.15.6

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,62 @@ 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.15.6] - 2026-08-10
12
+
13
+ ### Fixed
14
+
15
+ - **Eight user-visible strings could not be translated at all** — they had no
16
+ label key and no prop, so a consumer supplying a full `labels` dictionary
17
+ still shipped English on a Dutch page. Every one now resolves through
18
+ `getLabel` with the previous text as the fallback, so this is additive: a
19
+ consumer that supplies none of the new keys renders exactly as before.
20
+
21
+ | Component | Was | New key |
22
+ |---|---|---|
23
+ | `CategoryDescription` | Read more / Read less | `readMore`, `readLess` |
24
+ | `ProductDescription` | Read more / Read less | `readMore`, `readLess` |
25
+ | `MachineGrid` | Loading…, No machines found. | `loading`, `noMachines` |
26
+ | `RegisterForm` | Please select an account type. | `selectUserType` |
27
+ | `AccountIconAndMenu` | Hi, {name} | `greeting` |
28
+
29
+ `ProductTabs` now forwards its `labels` to `ProductDescription`, without
30
+ which the two new keys could not be reached on a product page.
31
+
32
+ The greeting takes a `{name}` placeholder rather than a bare prefix, so a
33
+ translation can put the name first — matching the existing `{link}`
34
+ convention in `LoginForm.noAccount`.
35
+
36
+ - **Raw server errors are overridable.** `RegisterForm` rendered
37
+ `result.error` and `PurchaseAuthorizationConfigurator` rendered the
38
+ add-contact failure verbatim — unlocalised upstream text, often cryptic.
39
+ Supplying `registrationFailed` / `addContactFailed` now replaces them
40
+ outright, the same masking `LoginForm` already does with
41
+ `invalidCredentials`. Omit the key to keep the current pass-through.
42
+
43
+ - **Both authorization modal close buttons had no accessible name.** The
44
+ `✕` glyph and the SVG icon were the only content, so screen readers
45
+ announced punctuation or nothing. Both take an `aria-label` from the
46
+ existing `closeLabel` key, with the glyph/icon marked `aria-hidden`.
47
+
48
+ ## [0.15.5] - 2026-08-10
49
+
50
+ ### Fixed
51
+
52
+ - `useMenu` no longer drops categories that lack a translation in the active
53
+ language. The query filtered server-side — `names(language: $language)` — so a
54
+ category with no entry for that language came back with `names: []` and
55
+ `slugs: []`. `mapCategory`'s `?? raw.names?.[0]` fallback then had nothing to
56
+ fall back to, and the row rendered with a blank label and an empty slug:
57
+ invisible and unclickable. Switching a bilingual storefront to a language with
58
+ partial translations collapsed the menu to whichever categories happened to be
59
+ translated (PWP-927).
60
+
61
+ `names` and `slugs` are now fetched unfiltered — every translation — and
62
+ `mapCategory` picks the active language, falling back to whichever translation
63
+ exists. `slugs` gained a `language` field so the slug follows the same rule as
64
+ the name. `$language` is gone from the query signature (an unused variable is
65
+ a GraphQL validation error); it still keys the cache and drives the pick.
66
+
11
67
  ## [0.15.4] - 2026-08-10
12
68
 
13
69
  ### Fixed
package/dist/index.cjs CHANGED
@@ -1592,15 +1592,15 @@ function buildCategoriesQuery(depth) {
1592
1592
  categories {
1593
1593
  categoryId
1594
1594
  hidden
1595
- names(language: $language) { value language }
1596
- slugs(language: $language) { value }
1595
+ names { value language }
1596
+ slugs { value language }
1597
1597
  ${buildCategoriesQuery(depth - 1)}
1598
1598
  }
1599
1599
  `;
1600
1600
  }
1601
1601
  function mapCategory(raw, language) {
1602
1602
  const nameEntry = raw.names?.find((n) => n.language === language) ?? raw.names?.[0];
1603
- const slugEntry = raw.slugs?.[0];
1603
+ const slugEntry = raw.slugs?.find((s) => s.language === language) ?? raw.slugs?.[0];
1604
1604
  return {
1605
1605
  categoryId: raw.categoryId,
1606
1606
  name: nameEntry?.value ?? "",
@@ -1676,17 +1676,17 @@ function useMenu(options) {
1676
1676
  inflightFetches.set(key, promise);
1677
1677
  try {
1678
1678
  const gql = `
1679
- query Menu($categoryId: Float, $language: String) {
1679
+ query Menu($categoryId: Float) {
1680
1680
  category(categoryId: $categoryId) {
1681
1681
  categoryId
1682
1682
  hidden
1683
- names(language: $language) { value language }
1684
- slugs(language: $language) { value }
1683
+ names { value language }
1684
+ slugs { value language }
1685
1685
  ${buildCategoriesQuery(depth)}
1686
1686
  }
1687
1687
  }
1688
1688
  `;
1689
- const variables = { categoryId: rootCategoryId, language };
1689
+ const variables = { categoryId: rootCategoryId };
1690
1690
  const data = await graphqlClient.query(gql, variables);
1691
1691
  const root = data?.category ?? null;
1692
1692
  const items = root ? (root.categories ?? []).filter((cat) => !isHidden(cat)).map((cat) => mapCategory(cat, language)) : [];
@@ -4181,10 +4181,7 @@ function AccountIconAndMenu(rawProps) {
4181
4181
  )
4182
4182
  }
4183
4183
  ),
4184
- props.user ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-account-menu__greeting hidden md:block font-normal", children: [
4185
- "Hi, ",
4186
- getUserName()
4187
- ] }) : null,
4184
+ props.user ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-account-menu__greeting hidden md:block font-normal", children: propellerV2CoreUi.getLabel(props.labels, "greeting", "Hi, {name}").replace("{name}", getUserName()) }) : null,
4188
4185
  !props.user ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-account-menu__greeting hidden md:block font-normal", children: propellerV2CoreUi.getLabel(props.labels, "accountLabel", "Account") }) : null
4189
4186
  ]
4190
4187
  }
@@ -7995,7 +7992,7 @@ function CategoryDescription(props) {
7995
7992
  type: "button",
7996
7993
  className: "propeller-category-description__toggle mt-2 text-sm font-medium text-primary hover:underline",
7997
7994
  onClick: () => setExpanded((v) => !v),
7998
- children: expanded ? "Read less" : "Read more"
7995
+ children: expanded ? propellerV2CoreUi.getLabel(props.labels, "readLess", "Read less") : propellerV2CoreUi.getLabel(props.labels, "readMore", "Read more")
7999
7996
  }
8000
7997
  ) : null
8001
7998
  ]
@@ -9912,7 +9909,7 @@ function GridToolbar(rawProps) {
9912
9909
  const [currentSortOrder, setCurrentSortOrder] = React6.useState(propellerSdkV2.SortOrder.DESC);
9913
9910
  const [currentOffset, setCurrentOffset] = React6.useState(12);
9914
9911
  const [currentViewMode, setCurrentViewMode] = React6.useState("grid");
9915
- function getLabel56(key) {
9912
+ function getLabel59(key) {
9916
9913
  const labels = props.labels || {};
9917
9914
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9918
9915
  }
@@ -9950,8 +9947,8 @@ function GridToolbar(rawProps) {
9950
9947
  }
9951
9948
  function getAvailabilityLabel() {
9952
9949
  const qty = props.minStock;
9953
- if (qty !== void 0 && qty > propellerV2CoreUi.MIN_STOCK_THRESHOLD) return `${getLabel56("inStock")}: ${qty}+`;
9954
- return getLabel56("inStock");
9950
+ if (qty !== void 0 && qty > propellerV2CoreUi.MIN_STOCK_THRESHOLD) return `${getLabel59("inStock")}: ${qty}+`;
9951
+ return getLabel59("inStock");
9955
9952
  }
9956
9953
  function handleSortFieldChange(field) {
9957
9954
  setCurrentSortField(field);
@@ -9997,7 +9994,7 @@ function GridToolbar(rawProps) {
9997
9994
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-grid-toolbar__count text-sm text-muted-foreground font-medium", children: props.itemsFound > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
9998
9995
  props.itemsFound,
9999
9996
  "\xA0",
10000
- props.itemsFound === 1 ? getLabel56("productSingular") : getLabel56("productPlural")
9997
+ props.itemsFound === 1 ? getLabel59("productSingular") : getLabel59("productPlural")
10001
9998
  ] }) : null }),
10002
9999
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
10003
10000
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -10008,7 +10005,7 @@ function GridToolbar(rawProps) {
10008
10005
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
10009
10006
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: n, children: [
10010
10007
  n,
10011
- getLabel56("perPage")
10008
+ getLabel59("perPage")
10012
10009
  ] }, n))
10013
10010
  }
10014
10011
  ),
@@ -10024,7 +10021,7 @@ function GridToolbar(rawProps) {
10024
10021
  {
10025
10022
  value: field,
10026
10023
  disabled: field === "PRICE" && isPriceSortDisabled(),
10027
- children: getLabel56(field)
10024
+ children: getLabel59(field)
10028
10025
  },
10029
10026
  field
10030
10027
  ))
@@ -10037,8 +10034,8 @@ function GridToolbar(rawProps) {
10037
10034
  value: currentSortOrder,
10038
10035
  onChange: (e) => handleSortOrderChange(e.target.value),
10039
10036
  children: [
10040
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel56("ASC") }),
10041
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel56("DESC") })
10037
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel59("ASC") }),
10038
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel59("DESC") })
10042
10039
  ]
10043
10040
  }
10044
10041
  ),
@@ -10048,7 +10045,7 @@ function GridToolbar(rawProps) {
10048
10045
  type: "button",
10049
10046
  className: "propeller-grid-toolbar__view-toggle h-9 w-9 flex items-center justify-center rounded-control border border-input bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors",
10050
10047
  onClick: (event) => handleViewChange(),
10051
- title: currentViewMode === "grid" ? getLabel56("switchToList") : getLabel56("switchToGrid"),
10048
+ title: currentViewMode === "grid" ? getLabel59("switchToList") : getLabel59("switchToGrid"),
10052
10049
  children: [
10053
10050
  currentViewMode === "grid" ? /* @__PURE__ */ jsxRuntime.jsxs(
10054
10051
  "svg",
@@ -10106,7 +10103,7 @@ function GridToolbar(rawProps) {
10106
10103
  onClick: (event) => {
10107
10104
  if (props.onClearFilters) props.onClearFilters();
10108
10105
  },
10109
- children: getLabel56("clearAll")
10106
+ children: getLabel59("clearAll")
10110
10107
  }
10111
10108
  ),
10112
10109
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
@@ -10117,7 +10114,7 @@ function GridToolbar(rawProps) {
10117
10114
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
10118
10115
  },
10119
10116
  children: [
10120
- getLabel56("price"),
10117
+ getLabel59("price"),
10121
10118
  ": \u20AC",
10122
10119
  props.priceFilterMin ?? 0,
10123
10120
  " \u2013 \u20AC",
@@ -10167,7 +10164,7 @@ var DEFAULT_LABELS2 = {
10167
10164
  of: "of"
10168
10165
  };
10169
10166
  function GridPagination(props) {
10170
- function getLabel56(key) {
10167
+ function getLabel59(key) {
10171
10168
  const labels = props.labels || {};
10172
10169
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
10173
10170
  }
@@ -10240,15 +10237,15 @@ function GridPagination(props) {
10240
10237
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--prev inline-flex items-center rounded-control border border-input bg-card px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
10241
10238
  disabled: currentPage === 1,
10242
10239
  onClick: () => handlePageChange(currentPage - 1),
10243
- children: getLabel56("previous")
10240
+ children: getLabel59("previous")
10244
10241
  }
10245
10242
  ),
10246
10243
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
10247
- getLabel56("page"),
10244
+ getLabel59("page"),
10248
10245
  "\xA0",
10249
10246
  currentPage,
10250
10247
  "\xA0",
10251
- getLabel56("of"),
10248
+ getLabel59("of"),
10252
10249
  "\xA0",
10253
10250
  totalPages
10254
10251
  ] }),
@@ -10259,7 +10256,7 @@ function GridPagination(props) {
10259
10256
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--next inline-flex items-center rounded-control border border-input bg-card px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
10260
10257
  disabled: currentPage === totalPages,
10261
10258
  onClick: () => handlePageChange(currentPage + 1),
10262
- children: getLabel56("next")
10259
+ children: getLabel59("next")
10263
10260
  }
10264
10261
  )
10265
10262
  ] }) : null,
@@ -10271,7 +10268,7 @@ function GridPagination(props) {
10271
10268
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--prev inline-flex items-center rounded-control border border-input bg-card px-3 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
10272
10269
  disabled: currentPage === 1,
10273
10270
  onClick: () => handlePageChange(currentPage - 1),
10274
- children: getLabel56("previous")
10271
+ children: getLabel59("previous")
10275
10272
  }
10276
10273
  ),
10277
10274
  getFullPages().map((item, idx) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -10298,7 +10295,7 @@ function GridPagination(props) {
10298
10295
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--next inline-flex items-center rounded-control border border-input bg-card px-3 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
10299
10296
  disabled: currentPage === totalPages,
10300
10297
  onClick: () => handlePageChange(currentPage + 1),
10301
- children: getLabel56("next")
10298
+ children: getLabel59("next")
10302
10299
  }
10303
10300
  )
10304
10301
  ] }) : null
@@ -10437,7 +10434,7 @@ function MachineGrid(rawProps) {
10437
10434
  const cards = rootMachines.map((m) => ({ machine: m, href: childSlugHref(m) })).filter((c) => c.href !== null);
10438
10435
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rawProps.className, children: [
10439
10436
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "propeller-grid-title mb-6 text-2xl font-semibold", children: rawProps.rootTitle ?? "Machines" }),
10440
- rootLoading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: "Loading\u2026" }) : cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: "No machines found." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: cards.map(({ machine: m, href }) => /* @__PURE__ */ jsxRuntime.jsx(
10437
+ rootLoading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: propellerV2CoreUi.getLabel(rawProps.machineCardLabels, "loading", "Loading\u2026") }) : cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-12 text-center text-foreground-subtle", children: propellerV2CoreUi.getLabel(rawProps.machineCardLabels, "noMachines", "No machines found.") }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: cards.map(({ machine: m, href }) => /* @__PURE__ */ jsxRuntime.jsx(
10441
10438
  MachineCard,
10442
10439
  {
10443
10440
  machine: m,
@@ -15436,7 +15433,7 @@ function ProductDescription(props) {
15436
15433
  type: "button",
15437
15434
  className: "propeller-product-description__toggle mt-2 text-sm font-medium text-primary hover:underline",
15438
15435
  onClick: () => setExpanded((v) => !v),
15439
- children: expanded ? "Read less" : "Read more"
15436
+ children: expanded ? propellerV2CoreUi.getLabel(props.labels, "readLess", "Read less") : propellerV2CoreUi.getLabel(props.labels, "readMore", "Read more")
15440
15437
  }
15441
15438
  ) : null
15442
15439
  ]
@@ -16751,7 +16748,8 @@ function ProductTabs(rawProps) {
16751
16748
  product: props.product,
16752
16749
  language: props.language,
16753
16750
  collapsed: props.descriptionCollapsed,
16754
- maxLength: props.descriptionMaxLength
16751
+ maxLength: props.descriptionMaxLength,
16752
+ labels: props.labels
16755
16753
  }
16756
16754
  ) : null,
16757
16755
  specsVisited && isTabVisible("specifications") ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: isActive("specifications") ? "" : "hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -16821,7 +16819,8 @@ function ProductTabs(rawProps) {
16821
16819
  product: props.product,
16822
16820
  language: props.language,
16823
16821
  collapsed: props.descriptionCollapsed,
16824
- maxLength: props.descriptionMaxLength
16822
+ maxLength: props.descriptionMaxLength,
16823
+ labels: props.labels
16825
16824
  }
16826
16825
  ) }) : null
16827
16826
  ] }) : null,
@@ -17149,8 +17148,9 @@ function PurchaseAuthorizationConfigurator(rawProps) {
17149
17148
  {
17150
17149
  type: "button",
17151
17150
  className: "text-muted-foreground hover:text-foreground transition",
17151
+ "aria-label": propellerV2CoreUi.getLabel(props.labels, "closeLabel", "Close"),
17152
17152
  onClick: () => closeAddContactModal(),
17153
- children: "\u2715"
17153
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" })
17154
17154
  }
17155
17155
  )
17156
17156
  ] }),
@@ -17248,7 +17248,7 @@ function PurchaseAuthorizationConfigurator(rawProps) {
17248
17248
  }
17249
17249
  )
17250
17250
  ] }),
17251
- addContactError ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: addContactError }) : null,
17251
+ addContactError ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: props.labels?.addContactFailed || addContactError }) : null,
17252
17252
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-3 pt-2", children: [
17253
17253
  /* @__PURE__ */ jsxRuntime.jsx(
17254
17254
  "button",
@@ -17401,8 +17401,9 @@ function PurchaseAuthorizationRequests(rawProps) {
17401
17401
  {
17402
17402
  type: "button",
17403
17403
  className: "propeller-purchase-authorization-requests__modal-close text-foreground-subtle hover:text-foreground focus:outline-none",
17404
+ "aria-label": propellerV2CoreUi.getLabel(props.labels, "closeLabel", "Close"),
17404
17405
  onClick: () => closeModal(),
17405
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", className: "h-5 w-5", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
17406
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": "true", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", className: "h-5 w-5", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
17406
17407
  }
17407
17408
  )
17408
17409
  ] }),
@@ -17685,6 +17686,8 @@ function RegisterForm(rawProps) {
17685
17686
  const emailPlaceholder = props.labels?.emailPlaceholder || "name@example.com";
17686
17687
  const passwordPlaceholder = props.labels?.passwordPlaceholder || "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
17687
17688
  const passwordMismatchText = props.labels?.passwordMismatch || "Passwords do not match";
17689
+ const selectUserTypeText = props.labels?.selectUserType || "Please select an account type.";
17690
+ const registrationFailedText = props.labels?.registrationFailed;
17688
17691
  const loginText = props.labels?.loginText || "Already have an account?";
17689
17692
  const loginLinkText = props.labels?.loginLink || "Log in";
17690
17693
  function isFieldRequired(fieldName) {
@@ -17695,7 +17698,7 @@ function RegisterForm(rawProps) {
17695
17698
  async function handleSubmit(e) {
17696
17699
  e.preventDefault();
17697
17700
  if (!effectiveUserType) {
17698
- setError("Please select an account type.");
17701
+ setError(selectUserTypeText);
17699
17702
  return;
17700
17703
  }
17701
17704
  if (password !== confirmPassword) {
@@ -17771,7 +17774,7 @@ function RegisterForm(rawProps) {
17771
17774
  );
17772
17775
  }
17773
17776
  } else if (result.error) {
17774
- setError(result.error);
17777
+ setError(registrationFailedText || result.error);
17775
17778
  }
17776
17779
  }
17777
17780
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-register-form", "data-loading": loading ? "true" : "false", "data-user-type": selectedUserType, children: [