@ikas/code-components-mcp 1.4.0-beta.139 → 1.4.0-beta.140

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.
Files changed (28) hide show
  1. package/data/framework.json +2 -32
  2. package/data/section-templates/add-to-cart/index.tsx +2 -16
  3. package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +3 -11
  4. package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +24 -57
  5. package/data/section-templates/cart-section/sub-components/CartItem/styles.css +0 -14
  6. package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +3 -11
  7. package/data/section-templates/header-section/sub-components/CartItem/index.tsx +24 -57
  8. package/data/section-templates/header-section/sub-components/CartItem/styles.css +0 -14
  9. package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +3 -11
  10. package/data/section-templates/image-handling/ikas-config-snippet.json +0 -1
  11. package/data/section-templates/navigation/sub-components/CartItem/index.tsx +24 -57
  12. package/data/section-templates/navigation/sub-components/CartItem/styles.css +0 -14
  13. package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +3 -11
  14. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +2 -16
  15. package/data/section-templates/product-detail-section/ikas-config-snippet.json +0 -1
  16. package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +3 -11
  17. package/data/storefront-api.json +1 -1
  18. package/data/storefront-types.json +1 -1
  19. package/package.json +1 -1
  20. package/data/section-templates/add-to-cart/utils/cartLimits.ts +0 -23
  21. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +0 -92
  22. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +0 -159
  23. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +0 -141
  24. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +0 -47
  25. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +0 -25
  26. package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +0 -120
  27. package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +0 -70
  28. package/data/section-templates/product-detail-section/utils/cartLimits.ts +0 -23
@@ -251,7 +251,7 @@
251
251
  "product-detail-patterns": {
252
252
  "title": "Product Detail Patterns (Serel Reference)",
253
253
  "description": "Serel ProductDetail with 12 child components in two COMPONENT_LIST slots, ProductGallery local component, Breadcrumb navigation, and component slot architecture",
254
- "content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n### Stock Locations (informational)\n\n`getProductAvailableStockLocations(product)` and the `IkasStockLocation*` types expose per-location availability. These are **not** part of the standard product-detail purchase flow: the reference theme does not use them on the PDP, and fulfillment (which location ships or serves an order) is resolved at checkout. Build a \"find in store\" or pickup surface from them only if your store explicitly needs one; otherwise you can ignore them on the PDP.\n\n**Return-shape gotcha:** despite its TypeScript signature (`Promise<IkasProductStockLocations[]>`), at runtime `getProductAvailableStockLocations` resolves to the raw GraphQL response **object** `{ stockLocations: IkasProductStockLocations[] }` — NOT a bare array. Calling `.filter`/`.map` on the result directly throws `(...).filter is not a function`. Always unwrap first:\n\n```tsx\nconst result = await getProductAvailableStockLocations(product);\nconst rows = Array.isArray(result)\n ? result\n : ((result as { stockLocations?: IkasProductStockLocations[] })?.stockLocations ?? []);\n```\n\n### Key Pattern: Multiple COMPONENT_LIST Slots\n\nUsing two slots (`components` and `bottomComponents`) allows the store owner to arrange child components differently:\n- `components` — displayed next to the gallery in a side panel\n- `bottomComponents` — displayed full-width below the gallery\n\nThis pattern can be applied to any section that needs multiple layout regions.",
254
+ "content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n### Key Pattern: Multiple COMPONENT_LIST Slots\n\nUsing two slots (`components` and `bottomComponents`) allows the store owner to arrange child components differently:\n- `components` — displayed next to the gallery in a side panel\n- `bottomComponents` — displayed full-width below the gallery\n\nThis pattern can be applied to any section that needs multiple layout regions.",
255
255
  "tags": [
256
256
  "product",
257
257
  "detail",
@@ -266,36 +266,6 @@
266
266
  "serel"
267
267
  ]
268
268
  },
269
- "back-in-stock-patterns": {
270
- "title": "Back-in-Stock Notification Patterns",
271
- "description": "Notify-me-when-available flow for out-of-stock variants: the login-required vs email-form branches, the notification form lifecycle, and the persistent reminder-saved state",
272
- "content": "## Back-in-Stock Notification Patterns\n\nWhen a variant is out of stock, the add-to-cart CTA is a dead end unless you offer a \"notify me when it's back\" flow. ikas ships the full API for this, wired up in the `ProductDetailBackInStock` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailBackInStock\", kind: \"children\" })\n```\n\n### When to show it\n\nRender the notify-me UI only when the variant is out of stock AND back-in-stock is enabled — otherwise render nothing and let `ProductDetailAddToCart` own the CTA:\n\n```tsx\nconst selectedVariant = getSelectedProductVariant(product);\nconst isOutOfStock = !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\nif (!isOutOfStock) return null;\nif (!getProductVariantIsBackInStockEnabled(selectedVariant)) return null;\n```\n\n### Two branches\n\n`getProductVariantIsBackInStockCustomerLoginRequired(variant)` selects the flow:\n\n- **Login required** — a logged-in customer is registered immediately with their own email; an anonymous visitor is sent to login first.\n- **No login required** — collect an email through the notification form.\n\n```tsx\nconst handleNotify = async () => {\n if (getProductVariantIsBackInStockCustomerLoginRequired(selectedVariant)) {\n const email = customerStore.customer?.email;\n if (email) {\n await saveProductVariantBackInStockReminder(selectedVariant, email);\n } else {\n Router.navigateToPage(\"LOGIN\");\n }\n return;\n }\n setEmailModalOpen(true); // anonymous email form\n};\n```\n\n### The email form (form lifecycle)\n\nThe no-login branch uses the standard ikas form model — get the form, init once, drive the input, submit:\n\n```tsx\nconst form = getProductVariantBackInStockForm(variant);\nuseEffect(() => { initBackInStockNotificationForm(form, variant); }, [form, variant]);\n\n<Input\n value={form.email?.value ?? \"\"}\n onInput={(e) => setBackInStockNotificationFormEmail(form, (e.target as HTMLInputElement).value)}\n/>\n\n// returns boolean; sets form.isSubmitting / isSuccess / isFailure / responseMessage\nconst ok = await submitBackInStockNotificationForm(form);\n```\n\nWrap the form component in `observer()` so it re-renders as the customer types and as the status flags flip — it reads the MobX-backed form.\n\n### Persistent \"saved\" state\n\n`variant.isBackInStockReminderSaved` is `true` once a reminder exists for that variant. Seed your CTA from it, and reset when the selected variant changes:\n\n```tsx\nconst [saved, setSaved] = useState(false);\nuseEffect(() => { setSaved(!!selectedVariant?.isBackInStockReminderSaved); }, [selectedVariant?.id]);\n```\n\nShow a non-interactive \"we'll notify you\" label instead of the button when `saved` is true.\n\n### Function reference\n\n| Function | Purpose |\n|----------|---------|\n| `getProductVariantIsBackInStockEnabled(variant)` | Gate: is back-in-stock enabled for this variant |\n| `getProductVariantIsBackInStockCustomerLoginRequired(variant)` | Branch: login-required vs email-form |\n| `saveProductVariantBackInStockReminder(variant, email)` | Register a reminder directly (login branch) |\n| `getProductVariantBackInStockForm(variant)` | Get-or-init the notification form model |\n| `initBackInStockNotificationForm(form, variant)` | Reset form fields + validation flags |\n| `setBackInStockNotificationFormEmail(form, value)` | Update the email field (re-validates after first submit) |\n| `submitBackInStockNotificationForm(form)` | Validate + save; returns boolean, sets status flags |\n| `variant.isBackInStockReminderSaved` | Persisted flag: a reminder already exists |",
273
- "tags": [
274
- "back-in-stock",
275
- "notify-me",
276
- "out-of-stock",
277
- "variant",
278
- "form",
279
- "reminder",
280
- "product-detail",
281
- "conversion"
282
- ]
283
- },
284
- "product-options": {
285
- "title": "Product Options (Customization) Patterns",
286
- "description": "Personalization option sets: async load, type-aware rendering for all option types, conditional child options, file upload, and submit-time validation with reset",
287
- "content": "## Product Options (Customization) Patterns\n\nProduct options let customers personalize a product before adding to cart — text, checkbox, choice, color/date picker, and file uploads, including parent/child (conditional) options and per-option prices. The reference theme implements the whole matrix in the `ProductDetailOptionSet` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailOptionSet\", kind: \"children\" })\n```\n\n### Load, then render displayed options\n\nThe option set is fetched async — render nothing until it resolves, then render only the currently displayed options (child options appear or hide based on the parent selection):\n\n```tsx\nuseEffect(() => { if (product) getProductOptionSet(product).then(() => setLoaded(true)); }, [product]);\n\nconst optionSet = product.productOptionSet;\nconst displayed = getDisplayedOptions(optionSet) as IkasProductOption[];\n```\n\n### Type-aware rendering\n\nEach option has a type — do not render everything as a text input. Switch on the SDK type guards and render a dedicated control per type:\n\n| Guard | Control |\n|-------|---------|\n| `isTextOption` / `isTextAreaOption` | text input / textarea |\n| `isCheckboxOption` | checkbox |\n| `isChoiceOption` | choice (box / select / swatch by display style) |\n| `isColorPickerOption` | color picker |\n| `isDatePickerOption` | date picker |\n| `isFileOption` | file upload |\n\nChild options render recursively from `getDisplayedChildOptions(option)`.\n\n### File upload\n\nFile options carry extension and min/max-count constraints. Upload through the SDK and surface its errors (file size, file type, max files):\n\n```tsx\nawait productOptionFileUpload(option, files);\n```\n\n### Validation + reset\n\nValidate on submit (used by add-to-cart), and reset after a successful add:\n\n```tsx\n// in add-to-cart, before adding:\nif (!validateOptionSet(product.productOptionSet, errorText)) return; // dispatches \"ikas:show-option-errors\"\n\n// after a successful add:\ninitProductOptionSetValues(product.productOptionSet);\nwindow.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n```\n\nThe option-set component listens for `ikas:show-option-errors` / `ikas:reset-option-state` to toggle its error display; validation itself is `hasValidProductOptionSetValues(...)` under the hood.\n\n### Key rules\n\n1. Gate render on the async `getProductOptionSet(product)` resolving.\n2. Render from `getDisplayedOptions(...)`, never a hardcoded list — child visibility is dynamic.\n3. Switch on the type guards; each type has its own control.\n4. Validate with `validateOptionSet` at add-to-cart; reset with `initProductOptionSetValues` after a successful add.",
288
- "tags": [
289
- "product-options",
290
- "customization",
291
- "personalization",
292
- "option-set",
293
- "file-upload",
294
- "child-options",
295
- "validation",
296
- "product-detail"
297
- ]
298
- },
299
269
  "product-list-patterns": {
300
270
  "title": "Product List & Filtering Patterns",
301
271
  "description": "Category pages, product filtering with display-type-aware rendering (swatch, number range, checkbox), sorting, pagination, and search patterns",
@@ -315,7 +285,7 @@
315
285
  "cart-patterns": {
316
286
  "title": "Cart Management Patterns (Serel Reference)",
317
287
  "description": "Serel CartPage section with CartItem sub-component, OrderSummary/CouponCode/EmptyState local components, cartStore patterns, and checkout flow",
318
- "content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Quantity Limits (min/max per cart)\n\nA product's sales channel can cap how many units go into one cart. The limits live on the product's `salesChannels`, matched to the active channel:\n\n```tsx\nimport { IkasStorefrontConfig } from \"@ikas/bp-storefront\";\n\nconst channel = product.salesChannels?.find((sc) => sc.id === IkasStorefrontConfig.salesChannelId);\nconst min = channel?.minQuantityPerCart ?? 1;\nconst max = channel?.maxQuantityPerCart; // null/undefined = no cap\n```\n\nApply these to the quantity stepper: seed the initial quantity to `min`, and cap increments at `max`. `addItemToCart(variant, product, qty)` also clamps to the same limits internally — but silently, so enforce them in the UI too or the customer sees the number snap with no explanation.\n\n> `IkasStorefrontConfig.salesChannelId` is a property. The older `getSalesChannelId()` method does not exist in this SDK.\n\n### Cart Operation validationError (complete list)\n\n`addItemToCart` / `changeItemQuantity` return `{ success, validationError? }`. The full set of `validationError` values is exactly three:\n\n| Value | Meaning |\n|-------|---------|\n| `INSUFFICIENT_STOCK` | Not enough stock for the requested quantity |\n| `INVALID_PRODUCT_OPTION_VALUES` | Required product options not fully or validly selected |\n| `EMPTY_CART` | The operation needs a non-empty cart |\n\nThere is **no** `MAX_QUANTITY_PER_CART_*` / `MIN_QUANTITY_PER_CART_*` error — quantity limits are clamped silently (see above), never surfaced as a validation error. Prevent limit violations in the stepper rather than branching on error codes that do not exist.\n\n### Campaign Offer Lines in the Cart\n\nCampaign discounts surface as order-level adjustment rows, not as edits on the line items:\n\n```tsx\n{cart.orderAdjustments?.map((adj) => (\n <div key={adj.name} className=\"cart-adjustment\">\n <span>{adj.name}</span>\n <span>{formatCurrency(adj.amount, adj.currencyCode, adj.currencySymbol)}</span>\n </div>\n))}\n```\n\nWhen a campaign auto-adds a bundled or gift line (a \"buy 1 get N\" offer), `isOrderLineItemAutoCreated(cart, item)` returns `true` for that line. Use it to badge the line and hide its quantity/remove controls — the customer cannot edit an auto-created line directly:\n\n```tsx\nimport { isOrderLineItemAutoCreated } from \"@ikas/bp-storefront\";\n\n{lineItems.map((item) => (\n <CartItem key={item.id} item={item} readOnly={isOrderLineItemAutoCreated(cart, item)} />\n))}\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
288
+ "content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
319
289
  "tags": [
320
290
  "cart",
321
291
  "checkout",
@@ -10,14 +10,13 @@ import {
10
10
  IkasBundleSettings,
11
11
  IkasStorefrontConfig,
12
12
  } from "@ikas/bp-storefront";
13
- import { useEffect, useState } from "preact/hooks";
13
+ import { useState } from "preact/hooks";
14
14
  import { Props } from "./types";
15
15
  import Button from "../../sub-components/Button";
16
16
  import QuantitySelector from "../../sub-components/QuantitySelector";
17
17
  import PayWithIkas from "./components/PayWithIkas";
18
18
  import { isBundleOutOfStock } from "../../utils/bundle";
19
19
  import { validateOptionSet } from "../../utils/optionSet";
20
- import { getProductCartLimits } from "../../utils/cartLimits";
21
20
  import { showToast } from "../../utils/toast";
22
21
 
23
22
  export function ProductDetailAddToCart({
@@ -38,12 +37,6 @@ export function ProductDetailAddToCart({
38
37
  const [quantity, setQuantity] = useState(1);
39
38
  const [isAddingToCart, setIsAddingToCart] = useState(false);
40
39
 
41
- // Seed the quantity to the product's minimum-per-cart when the product changes.
42
- useEffect(() => {
43
- if (!product) return;
44
- setQuantity(getProductCartLimits(product).min);
45
- }, [product?.id]);
46
-
47
40
  if (!product) return null;
48
41
 
49
42
  const editLineID =
@@ -66,8 +59,6 @@ export function ProductDetailAddToCart({
66
59
 
67
60
  const isDisabled = isOutOfStock || isAddingToCart;
68
61
 
69
- const { min: minQuantity, max: maxQuantity } = getProductCartLimits(product);
70
-
71
62
  const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();
72
63
  const routing = IkasStorefrontConfig.getCurrentRouting();
73
64
  const showPayWithIkas =
@@ -128,12 +119,7 @@ export function ProductDetailAddToCart({
128
119
  >
129
120
  <div className="kombos-pd-atc__actions">
130
121
  {!hideQuantityInput && !isOutOfStock && (
131
- <QuantitySelector
132
- value={quantity}
133
- onChange={setQuantity}
134
- min={minQuantity}
135
- max={maxQuantity}
136
- />
122
+ <QuantitySelector value={quantity} onChange={setQuantity} />
137
123
  )}
138
124
  <Button
139
125
  variant="primary"
@@ -5,27 +5,20 @@ interface Props {
5
5
  value: number;
6
6
  onChange: (value: number) => void;
7
7
  size?: "default" | "sm";
8
- min?: number;
9
- max?: number;
10
8
  }
11
9
 
12
10
  export default function QuantitySelector({
13
11
  value,
14
12
  onChange,
15
13
  size = "default",
16
- min = 1,
17
- max,
18
14
  }: Props) {
19
- const canDecrease = value > min;
20
- const canIncrease = max == null || value < max;
21
-
22
15
  return (
23
16
  <div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
24
17
  <button
25
18
  type="button"
26
19
  className="kombos-pd__qty-btn"
27
- onClick={() => onChange(Math.max(min, value - 1))}
28
- disabled={!canDecrease}
20
+ onClick={() => onChange(Math.max(1, value - 1))}
21
+ disabled={value <= 1}
29
22
  aria-label="Decrease quantity"
30
23
  >
31
24
  <MinusSVG />
@@ -38,8 +31,7 @@ export default function QuantitySelector({
38
31
  <button
39
32
  type="button"
40
33
  className="kombos-pd__qty-btn"
41
- onClick={() => onChange(max == null ? value + 1 : Math.min(max, value + 1))}
42
- disabled={!canIncrease}
34
+ onClick={() => onChange(value + 1)}
43
35
  aria-label="Increase quantity"
44
36
  >
45
37
  <PlusSVG />
@@ -14,8 +14,6 @@ import {
14
14
  getOrderLineItemFormattedOverridenPriceWithQuantity,
15
15
  hasOrderLineItemDiscount,
16
16
  getOrderLineItemOverridenPriceWithQuantity,
17
- cartStore,
18
- isOrderLineItemAutoCreated,
19
17
  } from "@ikas/bp-storefront";
20
18
  import { resolveAspectRatio, resolveObjectFit } from "../../utils/media";
21
19
  import { cx } from "../../utils/cx";
@@ -27,7 +25,6 @@ interface CartItemProps {
27
25
  variant?: "sidebar" | "page";
28
26
  aspectRatio?: AspectRatio;
29
27
  objectFit?: ObjectFit;
30
- giftLabel?: string;
31
28
  }
32
29
 
33
30
  const CartItem = observer(function CartItem({
@@ -36,25 +33,9 @@ const CartItem = observer(function CartItem({
36
33
  variant = "sidebar",
37
34
  aspectRatio,
38
35
  objectFit,
39
- giftLabel = "Gift",
40
36
  }: CartItemProps) {
41
37
  const isPage = variant === "page";
42
38
 
43
- // Lines a campaign added automatically (e.g. a "buy 1 get 1" gift) cannot be
44
- // edited or removed on their own, so render them read-only with a badge.
45
- const isAutoCreated =
46
- !!cartStore.cart && isOrderLineItemAutoCreated(cartStore.cart, item);
47
- const giftBadge = isAutoCreated ? (
48
- <span className="kombos-cart-item__gift-badge text-xs-medium">
49
- {giftLabel}
50
- </span>
51
- ) : null;
52
- const staticQuantity = (
53
- <span className="kombos-cart-item__qty-static text-sm-medium">
54
- ×{item.quantity}
55
- </span>
56
- );
57
-
58
39
  const mediaStyle: Record<string, string> = {
59
40
  aspectRatio: resolveAspectRatio(aspectRatio),
60
41
  objectFit: resolveObjectFit(objectFit),
@@ -122,7 +103,6 @@ const CartItem = observer(function CartItem({
122
103
  <a href={href} className={nameClass}>
123
104
  {item.variant?.name}
124
105
  </a>
125
- {giftBadge}
126
106
  {variantValues.length > 0 && (
127
107
  <p className="kombos-cart-item__variant-text text-sm-regular">
128
108
  {variantValues
@@ -133,22 +113,19 @@ const CartItem = observer(function CartItem({
133
113
  </p>
134
114
  )}
135
115
  </div>
136
- {!isAutoCreated && (
137
- <button
138
- className="kombos-cart-item__remove"
139
- onClick={() => onRemove(item)}
140
- aria-label="Remove"
141
- >
142
- <TrashSVG />
143
- </button>
144
- )}
116
+ <button
117
+ className="kombos-cart-item__remove"
118
+ onClick={() => onRemove(item)}
119
+ aria-label="Remove"
120
+ >
121
+ <TrashSVG />
122
+ </button>
145
123
  </div>
146
124
  ) : (
147
125
  <>
148
126
  <a href={href} className={nameClass}>
149
127
  {item.variant?.name}
150
128
  </a>
151
- {giftBadge}
152
129
 
153
130
  {variantValues.length > 0 && (
154
131
  <div className="kombos-cart-item__variants">
@@ -174,15 +151,11 @@ const CartItem = observer(function CartItem({
174
151
 
175
152
  {isPage ? (
176
153
  <div className="kombos-cart-item__bottom">
177
- {isAutoCreated ? (
178
- staticQuantity
179
- ) : (
180
- <QuantitySelector
181
- size="sm"
182
- value={item.quantity}
183
- onChange={(qty) => changeItemQuantity(item, qty)}
184
- />
185
- )}
154
+ <QuantitySelector
155
+ size="sm"
156
+ value={item.quantity}
157
+ onChange={(qty) => changeItemQuantity(item, qty)}
158
+ />
186
159
  <div className="kombos-cart-item__prices">
187
160
  {hasDiscount && (
188
161
  <span className="kombos-cart-item__original-price text-sm-regular-strike">
@@ -220,24 +193,18 @@ const CartItem = observer(function CartItem({
220
193
  </div>
221
194
 
222
195
  <div className="kombos-cart-item__actions">
223
- {isAutoCreated ? (
224
- staticQuantity
225
- ) : (
226
- <>
227
- <QuantitySelector
228
- size="sm"
229
- value={item.quantity}
230
- onChange={(qty) => changeItemQuantity(item, qty)}
231
- />
232
- <button
233
- className="kombos-cart-item__remove"
234
- onClick={() => onRemove(item)}
235
- aria-label="Remove"
236
- >
237
- <TrashSVG />
238
- </button>
239
- </>
240
- )}
196
+ <QuantitySelector
197
+ size="sm"
198
+ value={item.quantity}
199
+ onChange={(qty) => changeItemQuantity(item, qty)}
200
+ />
201
+ <button
202
+ className="kombos-cart-item__remove"
203
+ onClick={() => onRemove(item)}
204
+ aria-label="Remove"
205
+ >
206
+ <TrashSVG />
207
+ </button>
241
208
  </div>
242
209
  </>
243
210
  )}
@@ -168,17 +168,3 @@
168
168
  flex-shrink: 0;
169
169
  }
170
170
 
171
- /* Auto-created (campaign gift) line: badge + read-only quantity */
172
- .kombos-cart-item__gift-badge {
173
- display: inline-flex;
174
- align-self: flex-start;
175
- padding: 0.125rem 0.5rem;
176
- border-radius: 999px;
177
- background: var(--kombos-gray-100);
178
- color: var(--kombos-gray-500);
179
- }
180
-
181
- .kombos-cart-item__qty-static {
182
- color: var(--kombos-gray-500);
183
- }
184
-
@@ -5,27 +5,20 @@ interface Props {
5
5
  value: number;
6
6
  onChange: (value: number) => void;
7
7
  size?: "default" | "sm";
8
- min?: number;
9
- max?: number;
10
8
  }
11
9
 
12
10
  export default function QuantitySelector({
13
11
  value,
14
12
  onChange,
15
13
  size = "default",
16
- min = 1,
17
- max,
18
14
  }: Props) {
19
- const canDecrease = value > min;
20
- const canIncrease = max == null || value < max;
21
-
22
15
  return (
23
16
  <div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
24
17
  <button
25
18
  type="button"
26
19
  className="kombos-pd__qty-btn"
27
- onClick={() => onChange(Math.max(min, value - 1))}
28
- disabled={!canDecrease}
20
+ onClick={() => onChange(Math.max(1, value - 1))}
21
+ disabled={value <= 1}
29
22
  aria-label="Decrease quantity"
30
23
  >
31
24
  <MinusSVG />
@@ -38,8 +31,7 @@ export default function QuantitySelector({
38
31
  <button
39
32
  type="button"
40
33
  className="kombos-pd__qty-btn"
41
- onClick={() => onChange(max == null ? value + 1 : Math.min(max, value + 1))}
42
- disabled={!canIncrease}
34
+ onClick={() => onChange(value + 1)}
43
35
  aria-label="Increase quantity"
44
36
  >
45
37
  <PlusSVG />
@@ -14,8 +14,6 @@ import {
14
14
  getOrderLineItemFormattedOverridenPriceWithQuantity,
15
15
  hasOrderLineItemDiscount,
16
16
  getOrderLineItemOverridenPriceWithQuantity,
17
- cartStore,
18
- isOrderLineItemAutoCreated,
19
17
  } from "@ikas/bp-storefront";
20
18
  import { resolveAspectRatio, resolveObjectFit } from "../../utils/media";
21
19
  import { cx } from "../../utils/cx";
@@ -27,7 +25,6 @@ interface CartItemProps {
27
25
  variant?: "sidebar" | "page";
28
26
  aspectRatio?: AspectRatio;
29
27
  objectFit?: ObjectFit;
30
- giftLabel?: string;
31
28
  }
32
29
 
33
30
  const CartItem = observer(function CartItem({
@@ -36,25 +33,9 @@ const CartItem = observer(function CartItem({
36
33
  variant = "sidebar",
37
34
  aspectRatio,
38
35
  objectFit,
39
- giftLabel = "Gift",
40
36
  }: CartItemProps) {
41
37
  const isPage = variant === "page";
42
38
 
43
- // Lines a campaign added automatically (e.g. a "buy 1 get 1" gift) cannot be
44
- // edited or removed on their own, so render them read-only with a badge.
45
- const isAutoCreated =
46
- !!cartStore.cart && isOrderLineItemAutoCreated(cartStore.cart, item);
47
- const giftBadge = isAutoCreated ? (
48
- <span className="kombos-cart-item__gift-badge text-xs-medium">
49
- {giftLabel}
50
- </span>
51
- ) : null;
52
- const staticQuantity = (
53
- <span className="kombos-cart-item__qty-static text-sm-medium">
54
- ×{item.quantity}
55
- </span>
56
- );
57
-
58
39
  const mediaStyle: Record<string, string> = {
59
40
  aspectRatio: resolveAspectRatio(aspectRatio),
60
41
  objectFit: resolveObjectFit(objectFit),
@@ -122,7 +103,6 @@ const CartItem = observer(function CartItem({
122
103
  <a href={href} className={nameClass}>
123
104
  {item.variant?.name}
124
105
  </a>
125
- {giftBadge}
126
106
  {variantValues.length > 0 && (
127
107
  <p className="kombos-cart-item__variant-text text-sm-regular">
128
108
  {variantValues
@@ -133,22 +113,19 @@ const CartItem = observer(function CartItem({
133
113
  </p>
134
114
  )}
135
115
  </div>
136
- {!isAutoCreated && (
137
- <button
138
- className="kombos-cart-item__remove"
139
- onClick={() => onRemove(item)}
140
- aria-label="Remove"
141
- >
142
- <TrashSVG />
143
- </button>
144
- )}
116
+ <button
117
+ className="kombos-cart-item__remove"
118
+ onClick={() => onRemove(item)}
119
+ aria-label="Remove"
120
+ >
121
+ <TrashSVG />
122
+ </button>
145
123
  </div>
146
124
  ) : (
147
125
  <>
148
126
  <a href={href} className={nameClass}>
149
127
  {item.variant?.name}
150
128
  </a>
151
- {giftBadge}
152
129
 
153
130
  {variantValues.length > 0 && (
154
131
  <div className="kombos-cart-item__variants">
@@ -174,15 +151,11 @@ const CartItem = observer(function CartItem({
174
151
 
175
152
  {isPage ? (
176
153
  <div className="kombos-cart-item__bottom">
177
- {isAutoCreated ? (
178
- staticQuantity
179
- ) : (
180
- <QuantitySelector
181
- size="sm"
182
- value={item.quantity}
183
- onChange={(qty) => changeItemQuantity(item, qty)}
184
- />
185
- )}
154
+ <QuantitySelector
155
+ size="sm"
156
+ value={item.quantity}
157
+ onChange={(qty) => changeItemQuantity(item, qty)}
158
+ />
186
159
  <div className="kombos-cart-item__prices">
187
160
  {hasDiscount && (
188
161
  <span className="kombos-cart-item__original-price text-sm-regular-strike">
@@ -220,24 +193,18 @@ const CartItem = observer(function CartItem({
220
193
  </div>
221
194
 
222
195
  <div className="kombos-cart-item__actions">
223
- {isAutoCreated ? (
224
- staticQuantity
225
- ) : (
226
- <>
227
- <QuantitySelector
228
- size="sm"
229
- value={item.quantity}
230
- onChange={(qty) => changeItemQuantity(item, qty)}
231
- />
232
- <button
233
- className="kombos-cart-item__remove"
234
- onClick={() => onRemove(item)}
235
- aria-label="Remove"
236
- >
237
- <TrashSVG />
238
- </button>
239
- </>
240
- )}
196
+ <QuantitySelector
197
+ size="sm"
198
+ value={item.quantity}
199
+ onChange={(qty) => changeItemQuantity(item, qty)}
200
+ />
201
+ <button
202
+ className="kombos-cart-item__remove"
203
+ onClick={() => onRemove(item)}
204
+ aria-label="Remove"
205
+ >
206
+ <TrashSVG />
207
+ </button>
241
208
  </div>
242
209
  </>
243
210
  )}
@@ -168,17 +168,3 @@
168
168
  flex-shrink: 0;
169
169
  }
170
170
 
171
- /* Auto-created (campaign gift) line: badge + read-only quantity */
172
- .kombos-cart-item__gift-badge {
173
- display: inline-flex;
174
- align-self: flex-start;
175
- padding: 0.125rem 0.5rem;
176
- border-radius: 999px;
177
- background: var(--kombos-gray-100);
178
- color: var(--kombos-gray-500);
179
- }
180
-
181
- .kombos-cart-item__qty-static {
182
- color: var(--kombos-gray-500);
183
- }
184
-
@@ -5,27 +5,20 @@ interface Props {
5
5
  value: number;
6
6
  onChange: (value: number) => void;
7
7
  size?: "default" | "sm";
8
- min?: number;
9
- max?: number;
10
8
  }
11
9
 
12
10
  export default function QuantitySelector({
13
11
  value,
14
12
  onChange,
15
13
  size = "default",
16
- min = 1,
17
- max,
18
14
  }: Props) {
19
- const canDecrease = value > min;
20
- const canIncrease = max == null || value < max;
21
-
22
15
  return (
23
16
  <div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
24
17
  <button
25
18
  type="button"
26
19
  className="kombos-pd__qty-btn"
27
- onClick={() => onChange(Math.max(min, value - 1))}
28
- disabled={!canDecrease}
20
+ onClick={() => onChange(Math.max(1, value - 1))}
21
+ disabled={value <= 1}
29
22
  aria-label="Decrease quantity"
30
23
  >
31
24
  <MinusSVG />
@@ -38,8 +31,7 @@ export default function QuantitySelector({
38
31
  <button
39
32
  type="button"
40
33
  className="kombos-pd__qty-btn"
41
- onClick={() => onChange(max == null ? value + 1 : Math.min(max, value + 1))}
42
- disabled={!canIncrease}
34
+ onClick={() => onChange(value + 1)}
43
35
  aria-label="Increase quantity"
44
36
  >
45
37
  <PlusSVG />
@@ -39,7 +39,6 @@
39
39
  "<id-of-ProductDetailProductGroup>",
40
40
  "<id-of-ProductDetailVariant>",
41
41
  "<id-of-ProductDetailAddToCart>",
42
- "<id-of-ProductDetailBackInStock>",
43
42
  "<id-of-ProductDetailFeatures>",
44
43
  "<id-of-ProductDetailDescription>",
45
44
  "<id-of-ProductDetailBundleProduct>",