@propeller-commerce/propeller-v2-react-ui 0.14.0 → 0.15.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,39 @@ 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.0] - 2026-08-06
12
+
13
+ ### Changed
14
+
15
+ - **BREAKING — the stock filter is a toggle plus a quantity, not two
16
+ checkboxes.** `<GridFilters>` now takes `activeAvailability` as a single
17
+ `Availability` (`'all' | 'in-stock'`) rather than an array, gains
18
+ `activeMinStock`, and its `onAvailabilityChange` fires with both the
19
+ selection and the quantity. `<GridToolbar>` takes `availability` as a single
20
+ value plus `minStock`, and its `onAvailabilityFilterRemove` takes no
21
+ argument. `<ProductGrid>` takes `availability` plus `minStock`. The
22
+ out-of-stock bucket has no replacement. See [MIGRATION.md](./MIGRATION.md).
23
+
24
+ Two checkboxes could express "in stock or out of stock" — every product, and
25
+ so a state that looked active but sent no filter. They also could not answer
26
+ what a shopper asks next: not "is it stocked?" but "do you have enough?".
27
+
28
+ The section is now a two-state toggle and, once In stock is chosen, an
29
+ editable quantity with stepper buttons. The value can be typed directly or
30
+ stepped, commits on blur or Enter, and is floored and clamped to a minimum
31
+ of 1 — a lower threshold would match zero-stock products and contradict the
32
+ toggle. Switching back to All products clears it.
33
+
34
+ Requires `propeller-v2-core-ui` >= 0.6.0.
35
+
36
+ - **The toolbar shows one stock chip** instead of one per bucket, carrying the
37
+ quantity when it is above the default.
38
+
39
+ ### Added
40
+
41
+ - Label keys `allProducts`, `atLeast`, `pcs`, `quantityDecrease` and
42
+ `quantityIncrease`. The `outOfStock` key is no longer read.
43
+
11
44
  ## [0.14.0] - 2026-08-05
12
45
 
13
46
  ### Added
package/MIGRATION.md CHANGED
@@ -6,6 +6,69 @@ minor versions — each one is documented here.
6
6
 
7
7
  ---
8
8
 
9
+ ## `GridFilters` / `GridToolbar`: stock filter is a toggle plus a quantity
10
+
11
+ **When:** `0.15.0`.
12
+
13
+ The two stock checkboxes (In stock / Out of stock) are now a two-state toggle
14
+ plus an editable quantity — "in stock, at least N pcs". The out-of-stock
15
+ bucket has no replacement.
16
+
17
+ Two checkboxes could express "in stock or out of stock", which is every
18
+ product and so had to collapse to no filter — a state that looked active and
19
+ did nothing. They also could not express a quantity.
20
+
21
+ **`GridFilters`**
22
+
23
+ ```tsx
24
+ // before
25
+ <GridFilters
26
+ activeAvailability={['in-stock']} // Availability[]
27
+ onAvailabilityChange={(sel) => setAvailability(sel)}
28
+ />
29
+
30
+ // after
31
+ <GridFilters
32
+ activeAvailability={'in-stock'} // Availability ('all' | 'in-stock')
33
+ activeMinStock={5} // new, optional; undefined = 1
34
+ onAvailabilityChange={(sel, minStock) => { setAvailability(sel); setMinStock(minStock); }}
35
+ />
36
+ ```
37
+
38
+ **`GridToolbar`**
39
+
40
+ ```tsx
41
+ // before
42
+ <GridToolbar
43
+ availability={['in-stock']} // Availability[]
44
+ onAvailabilityFilterRemove={(value) => remove(value)}
45
+ />
46
+
47
+ // after
48
+ <GridToolbar
49
+ availability={'in-stock'} // Availability
50
+ minStock={5} // new, optional
51
+ onAvailabilityFilterRemove={() => reset()} // no argument — one chip
52
+ />
53
+ ```
54
+
55
+ **`ProductGrid`** takes `availability?: Availability` and `minStock?: number`
56
+ in place of `availability?: Availability[]`.
57
+
58
+ **Steps**
59
+
60
+ 1. Upgrade `@propeller-commerce/propeller-v2-core-ui` to `^0.6.0`, where
61
+ `Availability` becomes `'all' | 'in-stock'` and `buildInventoryFilter`
62
+ takes `(selection, minQuantity?)`.
63
+ 2. Replace array state with a single value plus a quantity number.
64
+ 3. Drop any out-of-stock option from your own UI and URL handling.
65
+ 4. If you serialise the selection into a URL, decide how to carry the
66
+ quantity. The boilerplates use `?availability=in-stock` for the default
67
+ and `?availability=in-stock:5` for a raised quantity, omitting the
68
+ parameter entirely for "all products".
69
+ 5. Add labels for the new keys: `allProducts`, `atLeast`, `pcs`,
70
+ `quantityDecrease`, `quantityIncrease`. Remove `outOfStock`.
71
+
9
72
  ## `Menu`: optional pre-fetched `tree` prop (additive)
10
73
 
11
74
  **When:** during `0.1.0` stabilization (pre-publish).
package/dist/index.cjs CHANGED
@@ -2262,7 +2262,8 @@ function useProductSearch(options) {
2262
2262
  JSON.stringify(options.textFilters ?? []),
2263
2263
  options.priceFilterMin ?? "",
2264
2264
  options.priceFilterMax ?? "",
2265
- JSON.stringify(options.availability ?? [])
2265
+ options.availability ?? "",
2266
+ options.minStock ?? ""
2266
2267
  ].join("|");
2267
2268
  if (inflightFetches2.has(inflightKey)) {
2268
2269
  fetchIdRef.current--;
@@ -2304,7 +2305,7 @@ function useProductSearch(options) {
2304
2305
  }
2305
2306
  ] : [];
2306
2307
  const priceFilter = options.priceFilterMin !== void 0 || options.priceFilterMax !== void 0 ? { from: options.priceFilterMin ?? 0, to: options.priceFilterMax ?? 999999 } : void 0;
2307
- const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability);
2308
+ const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability, options.minStock);
2308
2309
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2309
2310
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2310
2311
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
@@ -2405,6 +2406,7 @@ function useProductSearch(options) {
2405
2406
  options.priceFilterMin,
2406
2407
  options.priceFilterMax,
2407
2408
  options.availability,
2409
+ options.minStock,
2408
2410
  options.sortField,
2409
2411
  options.sortOrder,
2410
2412
  options.companyId,
@@ -2444,7 +2446,7 @@ function useProductSearch(options) {
2444
2446
  const userId = options.user && "contactId" in options.user ? options.user.contactId : options.user && "customerId" in options.user ? options.user.customerId : void 0;
2445
2447
  const contactId = options.user && "contactId" in options.user ? options.user.contactId : void 0;
2446
2448
  const customerId = options.user && "customerId" in options.user ? options.user.customerId : void 0;
2447
- const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability);
2449
+ const inventoryFilter = propellerV2CoreUi.buildInventoryFilter(options.availability, options.minStock);
2448
2450
  const categoryProductSearchInput = {
2449
2451
  language,
2450
2452
  page: 1,
@@ -2537,6 +2539,7 @@ function useProductSearch(options) {
2537
2539
  options.priceFilterMin,
2538
2540
  options.priceFilterMax,
2539
2541
  options.availability,
2542
+ options.minStock,
2540
2543
  options.sortField,
2541
2544
  options.sortOrder,
2542
2545
  pageSize,
@@ -9060,6 +9063,7 @@ function ProductGrid(rawProps) {
9060
9063
  priceFilterMin: props.priceFilterMin,
9061
9064
  priceFilterMax: props.priceFilterMax,
9062
9065
  availability: props.availability,
9066
+ minStock: props.minStock,
9063
9067
  sortField: props.sortField,
9064
9068
  sortOrder: props.sortOrder,
9065
9069
  pageSize: props.pageSize,
@@ -9363,7 +9367,13 @@ function GridFilters(rawProps) {
9363
9367
  const [appliedMin, setAppliedMin] = React6.useState(() => void 0);
9364
9368
  const [appliedMax, setAppliedMax] = React6.useState(() => void 0);
9365
9369
  const [availability, setAvailability] = React6.useState(
9366
- () => [...props.activeAvailability || []]
9370
+ () => props.activeAvailability || "all"
9371
+ );
9372
+ const [minStock, setMinStock] = React6.useState(
9373
+ () => props.activeMinStock ?? propellerV2CoreUi.MIN_STOCK_THRESHOLD
9374
+ );
9375
+ const [minStockInput, setMinStockInput] = React6.useState(
9376
+ () => String(props.activeMinStock ?? propellerV2CoreUi.MIN_STOCK_THRESHOLD)
9367
9377
  );
9368
9378
  function showPriceFilter() {
9369
9379
  const mode = props.portalMode || "open";
@@ -9418,13 +9428,28 @@ function GridFilters(rawProps) {
9418
9428
  props.onFilterChange(filter, value);
9419
9429
  if (props.getSelectedFilters) props.getSelectedFilters();
9420
9430
  }
9421
- function handleAvailabilityToggle(value, checked) {
9422
- const next = checked ? [...availability, value] : availability.filter((v) => v !== value);
9423
- setAvailability(next);
9431
+ function handleAvailabilityChange(value) {
9432
+ const nextMinStock = value === "all" ? propellerV2CoreUi.MIN_STOCK_THRESHOLD : minStock;
9433
+ setAvailability(value);
9434
+ setMinStock(nextMinStock);
9435
+ setMinStockInput(String(nextMinStock));
9436
+ setIsPending(true);
9437
+ if (props.onAvailabilityChange) props.onAvailabilityChange(value, nextMinStock);
9438
+ if (props.getSelectedFilters) props.getSelectedFilters();
9439
+ }
9440
+ function handleMinStockChange(value) {
9441
+ const n = Math.max(Math.floor(value) || propellerV2CoreUi.MIN_STOCK_THRESHOLD, propellerV2CoreUi.MIN_STOCK_THRESHOLD);
9442
+ setMinStock(n);
9443
+ setMinStockInput(String(n));
9444
+ if (n === minStock) return;
9424
9445
  setIsPending(true);
9425
- if (props.onAvailabilityChange) props.onAvailabilityChange(next);
9446
+ if (props.onAvailabilityChange) props.onAvailabilityChange(availability, n);
9426
9447
  if (props.getSelectedFilters) props.getSelectedFilters();
9427
9448
  }
9449
+ function commitMinStockInput() {
9450
+ const parsed = parseInt(minStockInput, 10);
9451
+ handleMinStockChange(isNaN(parsed) ? propellerV2CoreUi.MIN_STOCK_THRESHOLD : parsed);
9452
+ }
9428
9453
  function handleMinChange(value) {
9429
9454
  const n = value > currentMax ? currentMax : value;
9430
9455
  setCurrentMin(n);
@@ -9498,7 +9523,9 @@ function GridFilters(rawProps) {
9498
9523
  setCurrentMin(props.priceMin || 0);
9499
9524
  setCurrentMax(props.priceMax || 9999);
9500
9525
  setExpandedFilters({});
9501
- setAvailability([]);
9526
+ setAvailability("all");
9527
+ setMinStock(propellerV2CoreUi.MIN_STOCK_THRESHOLD);
9528
+ setMinStockInput(String(propellerV2CoreUi.MIN_STOCK_THRESHOLD));
9502
9529
  }, [props.clearSignal, props.priceMin, props.priceMax]);
9503
9530
  React6.useEffect(() => {
9504
9531
  if (!props.activeTextFilters) return;
@@ -9506,8 +9533,10 @@ function GridFilters(rawProps) {
9506
9533
  }, [props.activeTextFilters]);
9507
9534
  React6.useEffect(() => {
9508
9535
  if (!props.activeAvailability) return;
9509
- setAvailability([...props.activeAvailability]);
9510
- }, [props.activeAvailability]);
9536
+ setAvailability(props.activeAvailability);
9537
+ setMinStock(props.activeMinStock ?? propellerV2CoreUi.MIN_STOCK_THRESHOLD);
9538
+ setMinStockInput(String(props.activeMinStock ?? propellerV2CoreUi.MIN_STOCK_THRESHOLD));
9539
+ }, [props.activeAvailability, props.activeMinStock]);
9511
9540
  React6.useEffect(() => {
9512
9541
  if (props.activePriceMin === void 0 && props.activePriceMax === void 0) {
9513
9542
  setCurrentMin(props.priceMin || 0);
@@ -9610,34 +9639,76 @@ function GridFilters(rawProps) {
9610
9639
  showAvailability() ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9611
9640
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability space-y-3", children: [
9612
9641
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "propeller-grid-filters__availability-title text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: propellerV2CoreUi.getLabel(props.labels, "availability", "Availability") }),
9613
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability-options space-y-1.5", children: [
9614
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9642
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability-toggle inline-flex h-8 w-full rounded-control border border-input overflow-hidden", children: [
9643
+ /* @__PURE__ */ jsxRuntime.jsx(
9644
+ "button",
9645
+ {
9646
+ type: "button",
9647
+ className: `propeller-grid-filters__availability-option flex-1 text-xs font-medium transition-colors ${availability === "all" ? "bg-secondary text-secondary-foreground" : "bg-transparent text-muted-foreground hover:text-foreground"}`,
9648
+ "aria-pressed": availability === "all",
9649
+ disabled: !!props.isLoading,
9650
+ onClick: () => handleAvailabilityChange("all"),
9651
+ children: propellerV2CoreUi.getLabel(props.labels, "allProducts", "All products")
9652
+ }
9653
+ ),
9654
+ /* @__PURE__ */ jsxRuntime.jsx(
9655
+ "button",
9656
+ {
9657
+ type: "button",
9658
+ className: `propeller-grid-filters__availability-option flex-1 text-xs font-medium transition-colors ${availability === "in-stock" ? "bg-secondary text-secondary-foreground" : "bg-transparent text-muted-foreground hover:text-foreground"}`,
9659
+ "aria-pressed": availability === "in-stock",
9660
+ disabled: !!props.isLoading,
9661
+ onClick: () => handleAvailabilityChange("in-stock"),
9662
+ children: propellerV2CoreUi.getLabel(props.labels, "inStock", "In stock")
9663
+ }
9664
+ )
9665
+ ] }),
9666
+ availability === "in-stock" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability-quantity flex items-center gap-2 text-sm text-muted-foreground", children: [
9667
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: propellerV2CoreUi.getLabel(props.labels, "atLeast", "at least") }),
9668
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-filters__availability-stepper inline-flex items-center h-8 rounded-control border border-input bg-card overflow-hidden focus-within:ring-1 focus-within:ring-secondary", children: [
9615
9669
  /* @__PURE__ */ jsxRuntime.jsx(
9616
- "input",
9670
+ "button",
9617
9671
  {
9618
- type: "checkbox",
9619
- className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9620
- checked: availability.includes("in-stock"),
9621
- disabled: !!props.isLoading,
9622
- onChange: (e) => handleAvailabilityToggle("in-stock", e.target.checked)
9672
+ type: "button",
9673
+ className: "propeller-grid-filters__quantity-decrease h-full w-8 flex items-center justify-center text-muted-foreground disabled:opacity-40 disabled:cursor-not-allowed hover:bg-accent hover:text-foreground transition-colors",
9674
+ "aria-label": propellerV2CoreUi.getLabel(props.labels, "quantityDecrease", "Decrease quantity"),
9675
+ disabled: !!props.isLoading || minStock <= propellerV2CoreUi.MIN_STOCK_THRESHOLD,
9676
+ onClick: () => handleMinStockChange(minStock - 1),
9677
+ children: "\u2212"
9623
9678
  }
9624
9679
  ),
9625
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: propellerV2CoreUi.getLabel(props.labels, "inStock", "In stock") })
9626
- ] }),
9627
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "propeller-grid-filters__option flex items-center gap-2 cursor-pointer group", children: [
9628
9680
  /* @__PURE__ */ jsxRuntime.jsx(
9629
9681
  "input",
9630
9682
  {
9631
- type: "checkbox",
9632
- className: "propeller-grid-filters__checkbox h-4 w-4 rounded border-input text-secondary focus:ring-secondary cursor-pointer flex-shrink-0",
9633
- checked: availability.includes("out-of-stock"),
9683
+ type: "number",
9684
+ inputMode: "numeric",
9685
+ className: "propeller-grid-filters__availability-quantity-input w-10 h-full px-0 text-center font-medium text-foreground bg-transparent border-0 focus:outline-none focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
9686
+ "aria-label": propellerV2CoreUi.getLabel(props.labels, "atLeast", "at least"),
9687
+ min: propellerV2CoreUi.MIN_STOCK_THRESHOLD,
9688
+ step: 1,
9689
+ value: minStockInput,
9634
9690
  disabled: !!props.isLoading,
9635
- onChange: (e) => handleAvailabilityToggle("out-of-stock", e.target.checked)
9691
+ onChange: (e) => setMinStockInput(e.target.value),
9692
+ onBlur: () => commitMinStockInput(),
9693
+ onKeyDown: (e) => {
9694
+ if (e.key === "Enter") e.target.blur();
9695
+ }
9636
9696
  }
9637
9697
  ),
9638
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-filters__option-label flex-1 text-sm text-muted-foreground leading-none select-none group-hover:text-foreground", children: propellerV2CoreUi.getLabel(props.labels, "outOfStock", "Out of stock") })
9639
- ] })
9640
- ] })
9698
+ /* @__PURE__ */ jsxRuntime.jsx(
9699
+ "button",
9700
+ {
9701
+ type: "button",
9702
+ className: "propeller-grid-filters__quantity-increase h-full w-8 flex items-center justify-center text-muted-foreground disabled:opacity-40 disabled:cursor-not-allowed hover:bg-accent hover:text-foreground transition-colors",
9703
+ "aria-label": propellerV2CoreUi.getLabel(props.labels, "quantityIncrease", "Increase quantity"),
9704
+ disabled: !!props.isLoading,
9705
+ onClick: () => handleMinStockChange(minStock + 1),
9706
+ children: "+"
9707
+ }
9708
+ )
9709
+ ] }),
9710
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: propellerV2CoreUi.getLabel(props.labels, "pcs", "pcs") })
9711
+ ] }) : null
9641
9712
  ] }),
9642
9713
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-grid-filters__divider h-px bg-border-subtle" })
9643
9714
  ] }) : null,
@@ -9817,7 +9888,6 @@ var DEFAULT_LABELS = {
9817
9888
  perPage: " per page",
9818
9889
  price: "Price",
9819
9890
  inStock: "In stock",
9820
- outOfStock: "Out of stock",
9821
9891
  switchToList: "Switch to list view",
9822
9892
  switchToGrid: "Switch to grid view"
9823
9893
  };
@@ -9846,7 +9916,7 @@ function GridToolbar(rawProps) {
9846
9916
  const text = props.activeTextFilters || {};
9847
9917
  const hasText = Object.keys(text).some((k) => (text[k] || []).length > 0);
9848
9918
  const hasPrice = props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0;
9849
- const hasAvailability = (props.availability || []).length > 0;
9919
+ const hasAvailability = props.availability === "in-stock";
9850
9920
  return hasText || hasPrice || hasAvailability;
9851
9921
  }
9852
9922
  function getActiveFilterBadges() {
@@ -9865,8 +9935,10 @@ function GridToolbar(rawProps) {
9865
9935
  function isPriceSortDisabled() {
9866
9936
  return propellerV2CoreUi.isContentHidden(props.portalMode, props.user);
9867
9937
  }
9868
- function getAvailabilityLabel(value) {
9869
- return value === "in-stock" ? getLabel56("inStock") : getLabel56("outOfStock");
9938
+ function getAvailabilityLabel() {
9939
+ const qty = props.minStock;
9940
+ if (qty !== void 0 && qty > propellerV2CoreUi.MIN_STOCK_THRESHOLD) return `${getLabel56("inStock")}: ${qty}+`;
9941
+ return getLabel56("inStock");
9870
9942
  }
9871
9943
  function handleSortFieldChange(field) {
9872
9944
  setCurrentSortField(field);
@@ -10041,20 +10113,19 @@ function GridToolbar(rawProps) {
10041
10113
  ]
10042
10114
  }
10043
10115
  ) : null,
10044
- (props.availability || []).map((value) => /* @__PURE__ */ jsxRuntime.jsxs(
10116
+ props.availability === "in-stock" ? /* @__PURE__ */ jsxRuntime.jsxs(
10045
10117
  "span",
10046
10118
  {
10047
10119
  className: "propeller-grid-toolbar__filter-badge propeller-grid-toolbar__filter-badge--availability inline-flex items-center gap-1 cursor-pointer px-2.5 py-0.5 rounded-full text-xs font-semibold border border-input bg-background hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors",
10048
10120
  onClick: (event) => {
10049
- if (props.onAvailabilityFilterRemove) props.onAvailabilityFilterRemove(value);
10121
+ if (props.onAvailabilityFilterRemove) props.onAvailabilityFilterRemove();
10050
10122
  },
10051
10123
  children: [
10052
- getAvailabilityLabel(value),
10124
+ getAvailabilityLabel(),
10053
10125
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "propeller-grid-toolbar__filter-badge-remove", children: "\xD7" })
10054
10126
  ]
10055
- },
10056
- value
10057
- )),
10127
+ }
10128
+ ) : null,
10058
10129
  getActiveFilterBadges()?.map((badge) => /* @__PURE__ */ jsxRuntime.jsxs(
10059
10130
  "span",
10060
10131
  {