@numueg/theme-cli 0.6.0 → 0.6.1

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/index.js CHANGED
@@ -3828,6 +3828,14 @@ export default function ProductDetails({ settings }: SectionProps) {
3828
3828
  const productName = product.name;
3829
3829
  const productId = product.id;
3830
3830
 
3831
+ // Cap the quantity stepper to the selected variant's stock (the backend
3832
+ // enforces this too). No per-variant stock \u21D2 unbounded.
3833
+ const stockCap =
3834
+ activeVariant && typeof activeVariant.inventory_quantity === "number"
3835
+ ? Math.max(0, activeVariant.inventory_quantity)
3836
+ : Infinity;
3837
+ const maxQty = stockCap === Infinity ? Infinity : Math.max(1, stockCap);
3838
+
3831
3839
  const purchasable =
3832
3840
  product.in_stock &&
3833
3841
  (activeVariant?.is_in_stock ?? true) &&
@@ -3848,7 +3856,7 @@ export default function ProductDetails({ settings }: SectionProps) {
3848
3856
  .join("\\n");
3849
3857
  await updateNote((kept ? kept + "\\n" : "") + tag + pickedSize);
3850
3858
  }
3851
- await addItem(productId, activeVariant?.id, qty);
3859
+ await addItem(productId, activeVariant?.id, Math.min(qty, maxQty));
3852
3860
  } finally {
3853
3861
  setPending(false);
3854
3862
  }
@@ -3929,11 +3937,11 @@ export default function ProductDetails({ settings }: SectionProps) {
3929
3937
  <div className="flex items-center gap-3 mb-5">
3930
3938
  <span className="text-sm font-semibold">{t("Quantity", "\u0627\u0644\u0643\u0645\u064A\u0629")}</span>
3931
3939
  <div className="inline-flex items-center border rounded-md">
3932
- <button type="button" className="px-3 py-2" onClick={() => setQty((q) => Math.max(1, q - 1))}>
3940
+ <button type="button" className="px-3 py-2 disabled:opacity-40" disabled={qty <= 1} onClick={() => setQty((q) => Math.max(1, q - 1))}>
3933
3941
  \u2212
3934
3942
  </button>
3935
- <span className="px-3">{qty}</span>
3936
- <button type="button" className="px-3 py-2" onClick={() => setQty((q) => q + 1)}>
3943
+ <span className="px-3">{Math.min(qty, maxQty)}</span>
3944
+ <button type="button" className="px-3 py-2 disabled:opacity-40" disabled={qty >= maxQty} onClick={() => setQty((q) => Math.min(maxQty, q + 1))}>
3937
3945
  +
3938
3946
  </button>
3939
3947
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@numueg/theme-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "CLI for developing, validating, building, linting, and publishing NUMU storefront themes",
5
5
  "keywords": [
6
6
  "numu",
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import {
3
3
  useProductOptional,
4
4
  useVariantSelection,
@@ -98,6 +98,11 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
98
98
 
99
99
  const images = product.images ?? [];
100
100
  const mainImage = images[activeImg] ?? images[0];
101
+ // Stock to surface in the badge/button: the selected variant's when one is
102
+ // resolved (so "In stock" never sits next to a dead button), else product.
103
+ const displayInStock = activeVariant
104
+ ? (activeVariant.is_in_stock ?? activeVariant.in_stock ?? product.in_stock)
105
+ : product.in_stock;
101
106
  const purchasable =
102
107
  product.in_stock &&
103
108
  (activeVariant?.is_in_stock ?? true) &&
@@ -107,6 +112,20 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
107
112
  const productName = product.name;
108
113
  const selectedVariantId = activeVariant?.id ?? pickedId;
109
114
 
115
+ // Cap the quantity stepper to the selected variant's available stock so the
116
+ // buyer can't choose more than exists (the backend enforces this too; this
117
+ // keeps the UI honest). No per-variant stock ⇒ unbounded.
118
+ const stockCap =
119
+ activeVariant && typeof activeVariant.inventory_quantity === "number"
120
+ ? Math.max(0, activeVariant.inventory_quantity)
121
+ : Infinity;
122
+ const maxQty = stockCap === Infinity ? Infinity : Math.max(1, stockCap);
123
+ useEffect(() => {
124
+ setQty((q) => Math.min(q, maxQty));
125
+ }, [maxQty]);
126
+ const lowStockLeft =
127
+ stockCap !== Infinity && stockCap > 0 && stockCap <= 5 ? stockCap : null;
128
+
110
129
  const href = `/products/${product.slug}`;
111
130
  const shareUrl = shop?.formatUrl ? shop.formatUrl(href) : href;
112
131
  const enc = encodeURIComponent(shareUrl);
@@ -133,7 +152,7 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
133
152
  const next = `${kept ? `${kept}\n` : ""}${tag}${pickedSize}`;
134
153
  await updateNote(next);
135
154
  }
136
- await addItem(productId, selectedVariantId, qty);
155
+ await addItem(productId, selectedVariantId, Math.min(qty, maxQty));
137
156
  openCart();
138
157
  } finally {
139
158
  setPending(false);
@@ -220,8 +239,8 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
220
239
  ) : null}
221
240
  {formatMoney(activePrice, currency)}
222
241
  </p>
223
- <span className={`nt-pdp__stock${product.in_stock ? " is-in" : " is-out"}`}>
224
- {product.in_stock ? t("In stock", "متوفر") : t("Out of stock", "نفذ المخزون")}
242
+ <span className={`nt-pdp__stock${displayInStock ? " is-in" : " is-out"}`}>
243
+ {displayInStock ? t("In stock", "متوفر") : t("Out of stock", "نفذ المخزون")}
225
244
  <span className="nt-pdp__dot" aria-hidden />
226
245
  </span>
227
246
  </div>
@@ -341,14 +360,19 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
341
360
  <div className="nt-pdp__qtyblock">
342
361
  <span className="nt-pdp__opt-label">{t("Quantity", "الكمية")}</span>
343
362
  <div className="nt-qty nt-pdp__qty" aria-label={t("Quantity", "الكمية")}>
344
- <button type="button" aria-label={t("Decrease", "تقليل")} onClick={() => setQty((q) => Math.max(1, q - 1))}>
363
+ <button type="button" aria-label={t("Decrease", "تقليل")} disabled={qty <= 1} onClick={() => setQty((q) => Math.max(1, q - 1))}>
345
364
 
346
365
  </button>
347
- <span>{qty}</span>
348
- <button type="button" aria-label={t("Increase", "زيادة")} onClick={() => setQty((q) => q + 1)}>
366
+ <span>{Math.min(qty, maxQty)}</span>
367
+ <button type="button" aria-label={t("Increase", "زيادة")} disabled={qty >= maxQty} onClick={() => setQty((q) => Math.min(maxQty, q + 1))}>
349
368
  +
350
369
  </button>
351
370
  </div>
371
+ {lowStockLeft != null ? (
372
+ <span className="nt-pdp__hint">
373
+ {t(`Only ${lowStockLeft} left`, `باقي ${lowStockLeft} فقط`)}
374
+ </span>
375
+ ) : null}
352
376
  </div>
353
377
 
354
378
  {/* CTAs */}
@@ -361,7 +385,7 @@ export default function ProductDetails({ id, settings }: EmpSectionProps) {
361
385
  >
362
386
  {pending ? (
363
387
  "..."
364
- ) : !product.in_stock ? (
388
+ ) : !displayInStock ? (
365
389
  t("Sold out", "نفذ المخزون")
366
390
  ) : needsSize && !pickedSize ? (
367
391
  t("Select a size", "اختر المقاس")