@ikas/code-components-mcp 1.4.0-beta.2 → 1.4.0-beta.200
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/data/framework.json +208 -78
- package/data/migration.json +196 -25
- package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +16 -3
- package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +1 -1
- package/data/section-templates/account-info-section/ikas-config-snippet.json +24 -5
- package/data/section-templates/add-to-cart/ikas-config-snippet.json +17 -0
- package/data/section-templates/add-to-cart/index.tsx +16 -2
- package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/add-to-cart/utils/cartLimits.ts +24 -0
- package/data/section-templates/blog-home-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/blog-home-section/index.tsx +2 -1
- package/data/section-templates/blog-post-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/blog-post-section/index.tsx +3 -2
- package/data/section-templates/bundle-products/hooks/useBundleProducts.ts +7 -6
- package/data/section-templates/bundle-products/ikas-config-snippet.json +20 -0
- package/data/section-templates/cart-section/components/EmptyState/index.tsx +1 -1
- package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/cart-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/cart-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/cart-section/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/category-images-section/children/CategoryImageItem/ikas-config-snippet.json +4 -0
- package/data/section-templates/category-images-section/ikas-config-snippet.json +16 -1
- package/data/section-templates/category-list-section/children/CardProductName/ikas-config-snippet.json +2 -1
- package/data/section-templates/category-list-section/ikas-config-snippet.json +24 -3
- package/data/section-templates/category-list-section/index.tsx +2 -1
- package/data/section-templates/component-renderer/additional/ProductDetail/index.tsx +5 -1
- package/data/section-templates/component-renderer/ikas-config-snippet.json +16 -3
- package/data/section-templates/favorites/ikas-config-snippet.json +18 -1
- package/data/section-templates/features-section/ikas-config-snippet.json +14 -1
- package/data/section-templates/footer-section/ikas-config-snippet.json +16 -1
- package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/index.tsx +1 -1
- package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +8 -1
- package/data/section-templates/header-section/children/CookieBar/ikas-config-snippet.json +3 -0
- package/data/section-templates/header-section/children/Navbar/components/CartSidebar/index.tsx +2 -1
- package/data/section-templates/header-section/children/Navbar/components/SearchModal/index.tsx +2 -1
- package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +30 -3
- package/data/section-templates/header-section/children/Navbar/index.tsx +6 -5
- package/data/section-templates/header-section/ikas-config-snippet.json +16 -3
- package/data/section-templates/header-section/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/header-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/header-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/header-section/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/hero-slider-section/children/HeroSliderItem/ikas-config-snippet.json +18 -0
- package/data/section-templates/hero-slider-section/children/HeroSliderItem/index.tsx +12 -7
- package/data/section-templates/hero-slider-section/ikas-config-snippet.json +14 -1
- package/data/section-templates/image-handling/ikas-config-snippet.json +66 -13
- package/data/section-templates/image-handling/index.tsx +5 -1
- package/data/section-templates/login-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/navigation/components/CartSidebar/index.tsx +2 -1
- package/data/section-templates/navigation/components/SearchModal/index.tsx +2 -1
- package/data/section-templates/navigation/ikas-config-snippet.json +30 -3
- package/data/section-templates/navigation/index.tsx +6 -5
- package/data/section-templates/navigation/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/navigation/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/navigation/utils/cartLimits.ts +24 -0
- package/data/section-templates/navigation/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/ikas-config-snippet.json +17 -0
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +16 -2
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +92 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +175 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +141 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +47 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +25 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/ikas-config-snippet.json +18 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/ikas-config-snippet.json +20 -0
- package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +28 -1
- package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +30 -1
- package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/ikas-config-snippet.json +18 -1
- package/data/section-templates/product-detail-section/children/ProductDetailOffer/ikas-config-snippet.json +20 -0
- package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailPrices/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailSku/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailVariant/ikas-config-snippet.json +17 -0
- package/data/section-templates/product-detail-section/hooks/useBundleProducts.ts +7 -6
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +66 -13
- package/data/section-templates/product-detail-section/index.tsx +5 -1
- package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +120 -0
- package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +70 -0
- package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/product-detail-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/product-pricing/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-slider-section/children/CardProductName/ikas-config-snippet.json +2 -1
- package/data/section-templates/product-slider-section/ikas-config-snippet.json +20 -3
- package/data/section-templates/register-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/variant-selection/ikas-config-snippet.json +17 -0
- package/data/storefront-api.json +819 -1443
- package/data/storefront-types.json +9644 -6132
- package/dist/index.js +2978 -347
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,19 @@
|
|
|
34
34
|
"displayName": "Components",
|
|
35
35
|
"type": "COMPONENT_LIST",
|
|
36
36
|
"required": false,
|
|
37
|
+
"defaultValue": {
|
|
38
|
+
"components": [
|
|
39
|
+
{
|
|
40
|
+
"codeComponentId": "nb34u3yu-card-product-name"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"codeComponentId": "nb34u3yu-card-product-variants"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"codeComponentId": "nb34u3yu-card-product-price"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
37
50
|
"privateVarMap": {
|
|
38
51
|
"product": {
|
|
39
52
|
"id": "pvm_1772803530689_1",
|
|
@@ -41,9 +54,9 @@
|
|
|
41
54
|
}
|
|
42
55
|
},
|
|
43
56
|
"filteredComponentIds": [
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
57
|
+
"<id-of-CardProductPrice>",
|
|
58
|
+
"<id-of-CardProductVariants>",
|
|
59
|
+
"<id-of-CardProductName>"
|
|
47
60
|
]
|
|
48
61
|
}
|
|
49
62
|
],
|
|
@@ -50,12 +50,31 @@
|
|
|
50
50
|
"displayName": "Components",
|
|
51
51
|
"type": "COMPONENT_LIST",
|
|
52
52
|
"required": false,
|
|
53
|
+
"defaultValue": {
|
|
54
|
+
"components": [
|
|
55
|
+
{
|
|
56
|
+
"codeComponentId": "nb34u3yu-account-info-content"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"codeComponentId": "nb34u3yu-account-orders"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"codeComponentId": "nb34u3yu-account-addresses"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"codeComponentId": "nb34u3yu-account-favorites"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"codeComponentId": "nb34u3yu-account-order-detail"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
53
72
|
"filteredComponentIds": [
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
73
|
+
"<id-of-AccountInfoContent>",
|
|
74
|
+
"<id-of-AccountOrders>",
|
|
75
|
+
"<id-of-AccountAddresses>",
|
|
76
|
+
"<id-of-AccountFavorites>",
|
|
77
|
+
"<id-of-AccountOrderDetail>"
|
|
59
78
|
]
|
|
60
79
|
},
|
|
61
80
|
{
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"displayName": "Top Spacing",
|
|
41
41
|
"type": "TYPE",
|
|
42
42
|
"required": false,
|
|
43
|
+
"defaultValue": {
|
|
44
|
+
"value": 0,
|
|
45
|
+
"unit": "px"
|
|
46
|
+
},
|
|
43
47
|
"groupId": "mobile",
|
|
44
48
|
"typeId": "@ikas/bp-storefront-models-MarginTopStyleType"
|
|
45
49
|
},
|
|
@@ -48,6 +52,10 @@
|
|
|
48
52
|
"displayName": "Bottom Spacing",
|
|
49
53
|
"type": "TYPE",
|
|
50
54
|
"required": false,
|
|
55
|
+
"defaultValue": {
|
|
56
|
+
"value": 0,
|
|
57
|
+
"unit": "px"
|
|
58
|
+
},
|
|
51
59
|
"groupId": "mobile",
|
|
52
60
|
"typeId": "@ikas/bp-storefront-models-MarginBottomStyleType"
|
|
53
61
|
},
|
|
@@ -56,6 +64,10 @@
|
|
|
56
64
|
"displayName": "Top Spacing",
|
|
57
65
|
"type": "TYPE",
|
|
58
66
|
"required": false,
|
|
67
|
+
"defaultValue": {
|
|
68
|
+
"value": 0,
|
|
69
|
+
"unit": "px"
|
|
70
|
+
},
|
|
59
71
|
"groupId": "desktop",
|
|
60
72
|
"typeId": "@ikas/bp-storefront-models-MarginTopStyleType"
|
|
61
73
|
},
|
|
@@ -64,6 +76,10 @@
|
|
|
64
76
|
"displayName": "Bottom Spacing",
|
|
65
77
|
"type": "TYPE",
|
|
66
78
|
"required": false,
|
|
79
|
+
"defaultValue": {
|
|
80
|
+
"value": 0,
|
|
81
|
+
"unit": "px"
|
|
82
|
+
},
|
|
67
83
|
"groupId": "desktop",
|
|
68
84
|
"typeId": "@ikas/bp-storefront-models-MarginBottomStyleType"
|
|
69
85
|
},
|
|
@@ -72,6 +88,7 @@
|
|
|
72
88
|
"displayName": "Hide Quantity Selector",
|
|
73
89
|
"type": "BOOLEAN",
|
|
74
90
|
"required": false,
|
|
91
|
+
"defaultValue": false,
|
|
75
92
|
"groupId": "settings"
|
|
76
93
|
},
|
|
77
94
|
{
|
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
IkasBundleSettings,
|
|
11
11
|
IkasStorefrontConfig,
|
|
12
12
|
} from "@ikas/bp-storefront";
|
|
13
|
-
import { useState } from "preact/hooks";
|
|
13
|
+
import { useEffect, 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";
|
|
20
21
|
import { showToast } from "../../utils/toast";
|
|
21
22
|
|
|
22
23
|
export function ProductDetailAddToCart({
|
|
@@ -37,6 +38,12 @@ export function ProductDetailAddToCart({
|
|
|
37
38
|
const [quantity, setQuantity] = useState(1);
|
|
38
39
|
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
|
39
40
|
|
|
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
|
+
|
|
40
47
|
if (!product) return null;
|
|
41
48
|
|
|
42
49
|
const editLineID =
|
|
@@ -59,6 +66,8 @@ export function ProductDetailAddToCart({
|
|
|
59
66
|
|
|
60
67
|
const isDisabled = isOutOfStock || isAddingToCart;
|
|
61
68
|
|
|
69
|
+
const { min: minQuantity, max: maxQuantity } = getProductCartLimits(product);
|
|
70
|
+
|
|
62
71
|
const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();
|
|
63
72
|
const routing = IkasStorefrontConfig.getCurrentRouting();
|
|
64
73
|
const showPayWithIkas =
|
|
@@ -119,7 +128,12 @@ export function ProductDetailAddToCart({
|
|
|
119
128
|
>
|
|
120
129
|
<div className="kombos-pd-atc__actions">
|
|
121
130
|
{!hideQuantityInput && !isOutOfStock && (
|
|
122
|
-
<QuantitySelector
|
|
131
|
+
<QuantitySelector
|
|
132
|
+
value={quantity}
|
|
133
|
+
onChange={setQuantity}
|
|
134
|
+
min={minQuantity}
|
|
135
|
+
max={maxQuantity}
|
|
136
|
+
/>
|
|
123
137
|
)}
|
|
124
138
|
<Button
|
|
125
139
|
variant="primary"
|
|
@@ -5,20 +5,27 @@ interface Props {
|
|
|
5
5
|
value: number;
|
|
6
6
|
onChange: (value: number) => void;
|
|
7
7
|
size?: "default" | "sm";
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export default function QuantitySelector({
|
|
11
13
|
value,
|
|
12
14
|
onChange,
|
|
13
15
|
size = "default",
|
|
16
|
+
min = 1,
|
|
17
|
+
max,
|
|
14
18
|
}: Props) {
|
|
19
|
+
const canDecrease = value > min;
|
|
20
|
+
const canIncrease = max == null || value < max;
|
|
21
|
+
|
|
15
22
|
return (
|
|
16
23
|
<div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
|
|
17
24
|
<button
|
|
18
25
|
type="button"
|
|
19
26
|
className="kombos-pd__qty-btn"
|
|
20
|
-
onClick={() => onChange(Math.max(
|
|
21
|
-
disabled={
|
|
27
|
+
onClick={() => onChange(Math.max(min, value - 1))}
|
|
28
|
+
disabled={!canDecrease}
|
|
22
29
|
aria-label="Decrease quantity"
|
|
23
30
|
>
|
|
24
31
|
<MinusSVG />
|
|
@@ -31,7 +38,8 @@ export default function QuantitySelector({
|
|
|
31
38
|
<button
|
|
32
39
|
type="button"
|
|
33
40
|
className="kombos-pd__qty-btn"
|
|
34
|
-
onClick={() => onChange(value + 1)}
|
|
41
|
+
onClick={() => onChange(max == null ? value + 1 : Math.min(max, value + 1))}
|
|
42
|
+
disabled={!canIncrease}
|
|
35
43
|
aria-label="Increase quantity"
|
|
36
44
|
>
|
|
37
45
|
<PlusSVG />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IkasProduct, IkasStorefrontConfig } from "@ikas/bp-storefront";
|
|
2
|
+
|
|
3
|
+
export interface CartQuantityLimits {
|
|
4
|
+
min: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the per-cart quantity limits for a product from its active sales
|
|
10
|
+
* channel. Falls back to a minimum of 1 and no maximum when the channel or its
|
|
11
|
+
* limits are not set. `addItemToCart` clamps to the same limits internally only
|
|
12
|
+
* when it creates a NEW cart line — incrementing an existing line goes through
|
|
13
|
+
* `changeItemQuantity`, which never clamps — so enforcing these in the stepper
|
|
14
|
+
* is the only guard that covers both paths.
|
|
15
|
+
*/
|
|
16
|
+
export function getProductCartLimits(product: IkasProduct): CartQuantityLimits {
|
|
17
|
+
const channel = product.salesChannels?.find(
|
|
18
|
+
(sc) => sc.id === IkasStorefrontConfig.salesChannelId,
|
|
19
|
+
);
|
|
20
|
+
return {
|
|
21
|
+
min: channel?.minQuantityPerCart ?? 1,
|
|
22
|
+
max: channel?.maxQuantityPerCart ?? undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"displayName": "Aspect Ratio",
|
|
57
57
|
"type": "ENUM",
|
|
58
58
|
"required": false,
|
|
59
|
+
"defaultValue": "Square",
|
|
59
60
|
"groupId": "image-settings",
|
|
60
61
|
"enumTypeId": "{{ENUM_ID:AspectRatio}}"
|
|
61
62
|
},
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"displayName": "Image Fit",
|
|
65
66
|
"type": "ENUM",
|
|
66
67
|
"required": false,
|
|
68
|
+
"defaultValue": "Cover",
|
|
67
69
|
"groupId": "image-settings",
|
|
68
70
|
"enumTypeId": "{{ENUM_ID:ObjectFit}}"
|
|
69
71
|
},
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
hasBlogListPrevPage,
|
|
7
7
|
hasBlogListNextPage,
|
|
8
8
|
getIkasBlogCategoryHref,
|
|
9
|
+
withRoutePrefix,
|
|
9
10
|
} from "@ikas/bp-storefront";
|
|
10
11
|
import { observer } from "@ikas/component-utils";
|
|
11
12
|
import { cx } from "../../utils/cx";
|
|
@@ -47,7 +48,7 @@ export function BlogHome({
|
|
|
47
48
|
{categories.length > 0 && (
|
|
48
49
|
<div className="kombos-blog-home__categories">
|
|
49
50
|
<a
|
|
50
|
-
href="/blog"
|
|
51
|
+
href={withRoutePrefix("/blog")}
|
|
51
52
|
className={cx(
|
|
52
53
|
"kombos-blog-home__cat-btn",
|
|
53
54
|
"text-sm-medium",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"displayName": "Aspect Ratio",
|
|
42
42
|
"type": "ENUM",
|
|
43
43
|
"required": false,
|
|
44
|
+
"defaultValue": "Square",
|
|
44
45
|
"groupId": "appearance",
|
|
45
46
|
"enumTypeId": "{{ENUM_ID:AspectRatio}}"
|
|
46
47
|
},
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"displayName": "Image Fit",
|
|
50
51
|
"type": "ENUM",
|
|
51
52
|
"required": false,
|
|
53
|
+
"defaultValue": "Cover",
|
|
52
54
|
"groupId": "appearance",
|
|
53
55
|
"enumTypeId": "{{ENUM_ID:ObjectFit}}"
|
|
54
56
|
},
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getIkasBlogFormattedDate,
|
|
3
3
|
getIkasBlogCategoryHref,
|
|
4
4
|
getDefaultSrc,
|
|
5
|
+
withRoutePrefix,
|
|
5
6
|
} from "@ikas/bp-storefront";
|
|
6
7
|
|
|
7
8
|
import { Props } from "./types";
|
|
@@ -31,8 +32,8 @@ export function BlogPost({
|
|
|
31
32
|
const writerName = getFullName(blogPost.writer?.firstName, blogPost.writer?.lastName);
|
|
32
33
|
|
|
33
34
|
const breadcrumbItems: BreadcrumbItem[] = [
|
|
34
|
-
{ label: homeText, href: "/" },
|
|
35
|
-
{ label: blogText, href: "/blog" },
|
|
35
|
+
{ label: homeText, href: withRoutePrefix("/") },
|
|
36
|
+
{ label: blogText, href: withRoutePrefix("/blog") },
|
|
36
37
|
];
|
|
37
38
|
if (categoryName && categoryHref) {
|
|
38
39
|
breadcrumbItems.push({ label: categoryName, href: categoryHref });
|
|
@@ -9,15 +9,16 @@ import {
|
|
|
9
9
|
cartStore,
|
|
10
10
|
waitForCartStoreInit,
|
|
11
11
|
} from "@ikas/bp-storefront";
|
|
12
|
-
import { runInAction } from "mobx";
|
|
13
12
|
import { adjustBundleProductQuantity } from "../utils/bundle";
|
|
14
13
|
|
|
14
|
+
// No mobx action wrapper needed: adjustments go through the public
|
|
15
|
+
// setBundleProductQuantity API (plain mutation, same as the SDK's own usage
|
|
16
|
+
// examples), and keeping the theme free of direct mobx imports lets the
|
|
17
|
+
// scaffold install cleanly under pnpm's strict node_modules layout.
|
|
15
18
|
function adjustBundleQuantities(products: IkasBundleProduct[]) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
});
|
|
19
|
+
for (const bp of products) {
|
|
20
|
+
adjustBundleProductQuantity(bp);
|
|
21
|
+
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function useBundleProducts(product: IkasProduct | null | undefined) {
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"displayName": "Top Spacing",
|
|
17
17
|
"type": "TYPE",
|
|
18
18
|
"required": false,
|
|
19
|
+
"defaultValue": {
|
|
20
|
+
"value": 0,
|
|
21
|
+
"unit": "px"
|
|
22
|
+
},
|
|
19
23
|
"groupId": "mobile",
|
|
20
24
|
"typeId": "@ikas/bp-storefront-models-MarginTopStyleType"
|
|
21
25
|
},
|
|
@@ -24,6 +28,10 @@
|
|
|
24
28
|
"displayName": "Bottom Spacing",
|
|
25
29
|
"type": "TYPE",
|
|
26
30
|
"required": false,
|
|
31
|
+
"defaultValue": {
|
|
32
|
+
"value": 0,
|
|
33
|
+
"unit": "px"
|
|
34
|
+
},
|
|
27
35
|
"groupId": "mobile",
|
|
28
36
|
"typeId": "@ikas/bp-storefront-models-MarginBottomStyleType"
|
|
29
37
|
},
|
|
@@ -32,6 +40,10 @@
|
|
|
32
40
|
"displayName": "Top Spacing",
|
|
33
41
|
"type": "TYPE",
|
|
34
42
|
"required": false,
|
|
43
|
+
"defaultValue": {
|
|
44
|
+
"value": 0,
|
|
45
|
+
"unit": "px"
|
|
46
|
+
},
|
|
35
47
|
"groupId": "desktop",
|
|
36
48
|
"typeId": "@ikas/bp-storefront-models-MarginTopStyleType"
|
|
37
49
|
},
|
|
@@ -40,6 +52,10 @@
|
|
|
40
52
|
"displayName": "Bottom Spacing",
|
|
41
53
|
"type": "TYPE",
|
|
42
54
|
"required": false,
|
|
55
|
+
"defaultValue": {
|
|
56
|
+
"value": 0,
|
|
57
|
+
"unit": "px"
|
|
58
|
+
},
|
|
43
59
|
"groupId": "desktop",
|
|
44
60
|
"typeId": "@ikas/bp-storefront-models-MarginBottomStyleType"
|
|
45
61
|
},
|
|
@@ -48,6 +64,7 @@
|
|
|
48
64
|
"displayName": "Furniture Bundle Mode",
|
|
49
65
|
"type": "BOOLEAN",
|
|
50
66
|
"required": false,
|
|
67
|
+
"defaultValue": false,
|
|
51
68
|
"groupId": "settings"
|
|
52
69
|
},
|
|
53
70
|
{
|
|
@@ -55,6 +72,7 @@
|
|
|
55
72
|
"displayName": "Disable Product Link",
|
|
56
73
|
"type": "BOOLEAN",
|
|
57
74
|
"required": false,
|
|
75
|
+
"defaultValue": false,
|
|
58
76
|
"groupId": "settings"
|
|
59
77
|
},
|
|
60
78
|
{
|
|
@@ -86,6 +104,7 @@
|
|
|
86
104
|
"displayName": "Aspect Ratio",
|
|
87
105
|
"type": "ENUM",
|
|
88
106
|
"required": false,
|
|
107
|
+
"defaultValue": "Square",
|
|
89
108
|
"groupId": "appearance",
|
|
90
109
|
"enumTypeId": "{{ENUM_ID:AspectRatio}}"
|
|
91
110
|
},
|
|
@@ -94,6 +113,7 @@
|
|
|
94
113
|
"displayName": "Image Fit",
|
|
95
114
|
"type": "ENUM",
|
|
96
115
|
"required": false,
|
|
116
|
+
"defaultValue": "Cover",
|
|
97
117
|
"groupId": "appearance",
|
|
98
118
|
"enumTypeId": "{{ENUM_ID:ObjectFit}}"
|
|
99
119
|
}
|
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
getOrderLineItemFormattedOverridenPriceWithQuantity,
|
|
15
15
|
hasOrderLineItemDiscount,
|
|
16
16
|
getOrderLineItemOverridenPriceWithQuantity,
|
|
17
|
+
cartStore,
|
|
18
|
+
isOrderLineItemAutoCreated,
|
|
17
19
|
} from "@ikas/bp-storefront";
|
|
18
20
|
import { resolveAspectRatio, resolveObjectFit } from "../../utils/media";
|
|
19
21
|
import { cx } from "../../utils/cx";
|
|
22
|
+
import { useCartLineLimits } from "../../utils/cartLineLimits";
|
|
20
23
|
import type { AspectRatio, ObjectFit } from "../../global-types";
|
|
21
24
|
|
|
22
25
|
interface CartItemProps {
|
|
@@ -25,6 +28,7 @@ interface CartItemProps {
|
|
|
25
28
|
variant?: "sidebar" | "page";
|
|
26
29
|
aspectRatio?: AspectRatio;
|
|
27
30
|
objectFit?: ObjectFit;
|
|
31
|
+
giftLabel?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
const CartItem = observer(function CartItem({
|
|
@@ -33,9 +37,29 @@ const CartItem = observer(function CartItem({
|
|
|
33
37
|
variant = "sidebar",
|
|
34
38
|
aspectRatio,
|
|
35
39
|
objectFit,
|
|
40
|
+
giftLabel = "Gift",
|
|
36
41
|
}: CartItemProps) {
|
|
37
42
|
const isPage = variant === "page";
|
|
38
43
|
|
|
44
|
+
// The line's IkasOrderLineVariant carries no salesChannels, so the per-cart
|
|
45
|
+
// quantity limits come from a batched product fetch shared across all lines.
|
|
46
|
+
const limits = useCartLineLimits(item.variant?.productId);
|
|
47
|
+
|
|
48
|
+
// Lines a campaign added automatically (e.g. a "buy 1 get 1" gift) cannot be
|
|
49
|
+
// edited or removed on their own, so render them read-only with a badge.
|
|
50
|
+
const isAutoCreated =
|
|
51
|
+
!!cartStore.cart && isOrderLineItemAutoCreated(cartStore.cart, item);
|
|
52
|
+
const giftBadge = isAutoCreated ? (
|
|
53
|
+
<span className="kombos-cart-item__gift-badge text-xs-medium">
|
|
54
|
+
{giftLabel}
|
|
55
|
+
</span>
|
|
56
|
+
) : null;
|
|
57
|
+
const staticQuantity = (
|
|
58
|
+
<span className="kombos-cart-item__qty-static text-sm-medium">
|
|
59
|
+
×{item.quantity}
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
|
|
39
63
|
const mediaStyle: Record<string, string> = {
|
|
40
64
|
aspectRatio: resolveAspectRatio(aspectRatio),
|
|
41
65
|
objectFit: resolveObjectFit(objectFit),
|
|
@@ -103,6 +127,7 @@ const CartItem = observer(function CartItem({
|
|
|
103
127
|
<a href={href} className={nameClass}>
|
|
104
128
|
{item.variant?.name}
|
|
105
129
|
</a>
|
|
130
|
+
{giftBadge}
|
|
106
131
|
{variantValues.length > 0 && (
|
|
107
132
|
<p className="kombos-cart-item__variant-text text-sm-regular">
|
|
108
133
|
{variantValues
|
|
@@ -113,19 +138,22 @@ const CartItem = observer(function CartItem({
|
|
|
113
138
|
</p>
|
|
114
139
|
)}
|
|
115
140
|
</div>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
{!isAutoCreated && (
|
|
142
|
+
<button
|
|
143
|
+
className="kombos-cart-item__remove"
|
|
144
|
+
onClick={() => onRemove(item)}
|
|
145
|
+
aria-label="Remove"
|
|
146
|
+
>
|
|
147
|
+
<TrashSVG />
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
123
150
|
</div>
|
|
124
151
|
) : (
|
|
125
152
|
<>
|
|
126
153
|
<a href={href} className={nameClass}>
|
|
127
154
|
{item.variant?.name}
|
|
128
155
|
</a>
|
|
156
|
+
{giftBadge}
|
|
129
157
|
|
|
130
158
|
{variantValues.length > 0 && (
|
|
131
159
|
<div className="kombos-cart-item__variants">
|
|
@@ -151,11 +179,17 @@ const CartItem = observer(function CartItem({
|
|
|
151
179
|
|
|
152
180
|
{isPage ? (
|
|
153
181
|
<div className="kombos-cart-item__bottom">
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
{isAutoCreated ? (
|
|
183
|
+
staticQuantity
|
|
184
|
+
) : (
|
|
185
|
+
<QuantitySelector
|
|
186
|
+
size="sm"
|
|
187
|
+
value={item.quantity}
|
|
188
|
+
min={limits?.min ?? 1}
|
|
189
|
+
max={limits?.max}
|
|
190
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
159
193
|
<div className="kombos-cart-item__prices">
|
|
160
194
|
{hasDiscount && (
|
|
161
195
|
<span className="kombos-cart-item__original-price text-sm-regular-strike">
|
|
@@ -193,18 +227,26 @@ const CartItem = observer(function CartItem({
|
|
|
193
227
|
</div>
|
|
194
228
|
|
|
195
229
|
<div className="kombos-cart-item__actions">
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
{isAutoCreated ? (
|
|
231
|
+
staticQuantity
|
|
232
|
+
) : (
|
|
233
|
+
<>
|
|
234
|
+
<QuantitySelector
|
|
235
|
+
size="sm"
|
|
236
|
+
value={item.quantity}
|
|
237
|
+
min={limits?.min ?? 1}
|
|
238
|
+
max={limits?.max}
|
|
239
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
240
|
+
/>
|
|
241
|
+
<button
|
|
242
|
+
className="kombos-cart-item__remove"
|
|
243
|
+
onClick={() => onRemove(item)}
|
|
244
|
+
aria-label="Remove"
|
|
245
|
+
>
|
|
246
|
+
<TrashSVG />
|
|
247
|
+
</button>
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
208
250
|
</div>
|
|
209
251
|
</>
|
|
210
252
|
)}
|
|
@@ -168,3 +168,17 @@
|
|
|
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,20 +5,27 @@ interface Props {
|
|
|
5
5
|
value: number;
|
|
6
6
|
onChange: (value: number) => void;
|
|
7
7
|
size?: "default" | "sm";
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export default function QuantitySelector({
|
|
11
13
|
value,
|
|
12
14
|
onChange,
|
|
13
15
|
size = "default",
|
|
16
|
+
min = 1,
|
|
17
|
+
max,
|
|
14
18
|
}: Props) {
|
|
19
|
+
const canDecrease = value > min;
|
|
20
|
+
const canIncrease = max == null || value < max;
|
|
21
|
+
|
|
15
22
|
return (
|
|
16
23
|
<div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
|
|
17
24
|
<button
|
|
18
25
|
type="button"
|
|
19
26
|
className="kombos-pd__qty-btn"
|
|
20
|
-
onClick={() => onChange(Math.max(
|
|
21
|
-
disabled={
|
|
27
|
+
onClick={() => onChange(Math.max(min, value - 1))}
|
|
28
|
+
disabled={!canDecrease}
|
|
22
29
|
aria-label="Decrease quantity"
|
|
23
30
|
>
|
|
24
31
|
<MinusSVG />
|
|
@@ -31,7 +38,8 @@ export default function QuantitySelector({
|
|
|
31
38
|
<button
|
|
32
39
|
type="button"
|
|
33
40
|
className="kombos-pd__qty-btn"
|
|
34
|
-
onClick={() => onChange(value + 1)}
|
|
41
|
+
onClick={() => onChange(max == null ? value + 1 : Math.min(max, value + 1))}
|
|
42
|
+
disabled={!canIncrease}
|
|
35
43
|
aria-label="Increase quantity"
|
|
36
44
|
>
|
|
37
45
|
<PlusSVG />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IkasProduct, IkasStorefrontConfig } from "@ikas/bp-storefront";
|
|
2
|
+
|
|
3
|
+
export interface CartQuantityLimits {
|
|
4
|
+
min: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the per-cart quantity limits for a product from its active sales
|
|
10
|
+
* channel. Falls back to a minimum of 1 and no maximum when the channel or its
|
|
11
|
+
* limits are not set. `addItemToCart` clamps to the same limits internally only
|
|
12
|
+
* when it creates a NEW cart line — incrementing an existing line goes through
|
|
13
|
+
* `changeItemQuantity`, which never clamps — so enforcing these in the stepper
|
|
14
|
+
* is the only guard that covers both paths.
|
|
15
|
+
*/
|
|
16
|
+
export function getProductCartLimits(product: IkasProduct): CartQuantityLimits {
|
|
17
|
+
const channel = product.salesChannels?.find(
|
|
18
|
+
(sc) => sc.id === IkasStorefrontConfig.salesChannelId,
|
|
19
|
+
);
|
|
20
|
+
return {
|
|
21
|
+
min: channel?.minQuantityPerCart ?? 1,
|
|
22
|
+
max: channel?.maxQuantityPerCart ?? undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|