@ikas/code-components-mcp 2.6.1 → 2.7.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/data/framework.json +68 -8
- package/data/migration.json +9 -3
- 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 +23 -0
- package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +57 -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/header-section/sub-components/CartItem/index.tsx +57 -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/image-handling/ikas-config-snippet.json +1 -0
- package/data/section-templates/navigation/sub-components/CartItem/index.tsx +57 -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/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 +159 -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/ikas-config-snippet.json +1 -0
- 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 +23 -0
- package/data/storefront-api.json +82 -12
- package/data/storefront-types.json +2747 -22
- package/dist/index.js +582 -179
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// This file is auto-generated — do not edit manually.
|
|
2
|
+
import type {
|
|
3
|
+
IkasProduct,
|
|
4
|
+
MarginTopStyleType,
|
|
5
|
+
MarginBottomStyleType,
|
|
6
|
+
} from "@ikas/bp-storefront";
|
|
7
|
+
|
|
8
|
+
export interface Props {
|
|
9
|
+
product?: IkasProduct | null;
|
|
10
|
+
notifyButtonText?: string;
|
|
11
|
+
reminderSavedText?: string;
|
|
12
|
+
alreadySavedMessage?: string;
|
|
13
|
+
savedMessage?: string;
|
|
14
|
+
loginRequiredMessage?: string;
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
modalTitle?: string;
|
|
17
|
+
modalDescription?: string;
|
|
18
|
+
emailLabel?: string;
|
|
19
|
+
submitButtonText?: string;
|
|
20
|
+
submittingText?: string;
|
|
21
|
+
mobileMarginTop?: MarginTopStyleType;
|
|
22
|
+
mobileMarginBottom?: MarginBottomStyleType;
|
|
23
|
+
desktopMarginTop?: MarginTopStyleType;
|
|
24
|
+
desktopMarginBottom?: MarginBottomStyleType;
|
|
25
|
+
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"<id-of-ProductDetailProductGroup>",
|
|
40
40
|
"<id-of-ProductDetailVariant>",
|
|
41
41
|
"<id-of-ProductDetailAddToCart>",
|
|
42
|
+
"<id-of-ProductDetailBackInStock>",
|
|
42
43
|
"<id-of-ProductDetailFeatures>",
|
|
43
44
|
"<id-of-ProductDetailDescription>",
|
|
44
45
|
"<id-of-ProductDetailBundleProduct>",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useId } from "preact/hooks";
|
|
2
|
+
import { createPortal } from "preact/compat";
|
|
3
|
+
import type { ComponentChildren } from "preact";
|
|
4
|
+
import { useScrollLock } from "../../hooks/useScrollLock";
|
|
5
|
+
import { cx } from "../../utils/cx";
|
|
6
|
+
import { XSVG } from "../icons";
|
|
7
|
+
import Button, { type ButtonProps } from "../Button";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
title: string;
|
|
12
|
+
maxWidth?: string;
|
|
13
|
+
children: ComponentChildren;
|
|
14
|
+
className?: string;
|
|
15
|
+
okText?: string;
|
|
16
|
+
cancelText?: string;
|
|
17
|
+
onOk?: (e: Event) => void;
|
|
18
|
+
okButtonProps?: ButtonProps;
|
|
19
|
+
cancelButtonProps?: ButtonProps;
|
|
20
|
+
footer?: ComponentChildren | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function Modal({
|
|
24
|
+
onClose,
|
|
25
|
+
title,
|
|
26
|
+
maxWidth = "37.5rem",
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
okText,
|
|
30
|
+
cancelText,
|
|
31
|
+
onOk,
|
|
32
|
+
okButtonProps,
|
|
33
|
+
cancelButtonProps,
|
|
34
|
+
footer,
|
|
35
|
+
}: Props) {
|
|
36
|
+
const titleId = useId();
|
|
37
|
+
const [visible, setVisible] = useState(false);
|
|
38
|
+
const onCloseRef = useRef(onClose);
|
|
39
|
+
onCloseRef.current = onClose;
|
|
40
|
+
|
|
41
|
+
const handleCloseRef = useRef(() => {});
|
|
42
|
+
handleCloseRef.current = () => {
|
|
43
|
+
setVisible(false);
|
|
44
|
+
setTimeout(() => onCloseRef.current(), 300);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
useScrollLock();
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const rafId = requestAnimationFrame(() => setVisible(true));
|
|
51
|
+
return () => cancelAnimationFrame(rafId);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
56
|
+
if (e.key === "Escape") handleCloseRef.current();
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
59
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
return createPortal(
|
|
63
|
+
<div
|
|
64
|
+
className={cx("kombos-modal", visible && "kombos-modal--open", className)}
|
|
65
|
+
role="dialog"
|
|
66
|
+
aria-modal="true"
|
|
67
|
+
aria-labelledby={titleId}
|
|
68
|
+
>
|
|
69
|
+
<div className="kombos-modal__panel" style={{ maxWidth }}>
|
|
70
|
+
<div className="kombos-modal__header">
|
|
71
|
+
<h2 id={titleId} className="kombos-modal__title text-md-medium">
|
|
72
|
+
{title}
|
|
73
|
+
</h2>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="kombos-modal__close"
|
|
77
|
+
onClick={() => handleCloseRef.current()}
|
|
78
|
+
aria-label="Close"
|
|
79
|
+
>
|
|
80
|
+
<XSVG />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="kombos-modal__body">{children}</div>
|
|
85
|
+
|
|
86
|
+
{footer !== null &&
|
|
87
|
+
(footer !== undefined ? (
|
|
88
|
+
<div className="kombos-modal__footer">{footer}</div>
|
|
89
|
+
) : (
|
|
90
|
+
(cancelText || okText) && (
|
|
91
|
+
<div className="kombos-modal__footer">
|
|
92
|
+
{cancelText && (
|
|
93
|
+
<Button
|
|
94
|
+
variant="secondary"
|
|
95
|
+
size="s"
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => handleCloseRef.current()}
|
|
98
|
+
{...cancelButtonProps}
|
|
99
|
+
>
|
|
100
|
+
{cancelText}
|
|
101
|
+
</Button>
|
|
102
|
+
)}
|
|
103
|
+
{okText && (
|
|
104
|
+
<Button
|
|
105
|
+
variant="primary"
|
|
106
|
+
size="s"
|
|
107
|
+
onClick={onOk}
|
|
108
|
+
{...okButtonProps}
|
|
109
|
+
>
|
|
110
|
+
{okText}
|
|
111
|
+
</Button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
</div>,
|
|
118
|
+
document.body,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* ── Modal ── */
|
|
2
|
+
.kombos-modal {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
z-index: var(--kombos-z-overlay);
|
|
6
|
+
background: rgba(0, 0, 0, 0.35);
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
opacity: 0;
|
|
11
|
+
transition: opacity 0.3s;
|
|
12
|
+
padding: 1rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.kombos-modal--open {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.kombos-modal__panel {
|
|
20
|
+
background: var(--kombos-white);
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
width: 100%;
|
|
23
|
+
max-height: 90vh;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
transform: translateY(1rem);
|
|
27
|
+
transition: transform 0.3s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.kombos-modal--open .kombos-modal__panel {
|
|
31
|
+
transform: translateY(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.kombos-modal__header {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
align-items: center;
|
|
38
|
+
background: var(--kombos-white);
|
|
39
|
+
padding: 1.5rem;
|
|
40
|
+
border-bottom: 1px solid var(--kombos-gray-200);
|
|
41
|
+
border-radius: 6px 6px 0 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.kombos-modal__title {
|
|
45
|
+
color: var(--kombos-gray-900);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.kombos-modal__close {
|
|
49
|
+
background: none;
|
|
50
|
+
border: none;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
padding: 0;
|
|
53
|
+
color: var(--kombos-gray-700);
|
|
54
|
+
font-size: 1.25rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kombos-modal__body {
|
|
58
|
+
padding: 1.5rem;
|
|
59
|
+
overflow-y: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.kombos-modal__footer {
|
|
63
|
+
background: var(--kombos-white);
|
|
64
|
+
padding: 1.5rem;
|
|
65
|
+
border-top: 1px solid var(--kombos-gray-200);
|
|
66
|
+
border-radius: 0 0 6px 6px;
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 0.75rem;
|
|
69
|
+
justify-content: flex-end;
|
|
70
|
+
}
|
package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx
CHANGED
|
@@ -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,23 @@
|
|
|
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, so
|
|
12
|
+
* applying these in the stepper keeps the UI honest instead of letting the
|
|
13
|
+
* quantity snap silently.
|
|
14
|
+
*/
|
|
15
|
+
export function getProductCartLimits(product: IkasProduct): CartQuantityLimits {
|
|
16
|
+
const channel = product.salesChannels?.find(
|
|
17
|
+
(sc) => sc.id === IkasStorefrontConfig.salesChannelId,
|
|
18
|
+
);
|
|
19
|
+
return {
|
|
20
|
+
min: channel?.minQuantityPerCart ?? 1,
|
|
21
|
+
max: channel?.maxQuantityPerCart ?? undefined,
|
|
22
|
+
};
|
|
23
|
+
}
|
package/data/storefront-api.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-07-
|
|
2
|
+
"generatedAt": "2026-07-27T13:33:15.430Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListProductBrand",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"name": "getAttributeDetailValues",
|
|
168
168
|
"displayName": "getValues",
|
|
169
169
|
"signature": "function getAttributeDetailValues(attributeDetail: IkasProductAttributeDetail): IkasProductAttributeValue[]",
|
|
170
|
-
"description": "Retrieves the attribute values for a product attribute detail, merging variant and product-level attributes.",
|
|
170
|
+
"description": "Retrieves the attribute values for a product attribute detail, merging variant and product-level attributes.\nThis is how a PRODUCT_ATTRIBUTE code-component prop is consumed: that prop hands you the\nIkasProductAttributeDetail wrapper ({ product, attributePropValue }), never the attribute value itself, so\nreading `prop.value` silently yields nothing — call this function instead.",
|
|
171
171
|
"params": [
|
|
172
172
|
{
|
|
173
173
|
"name": "attributeDetail",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"name": "getAttributeListValues",
|
|
196
196
|
"displayName": "getValues",
|
|
197
197
|
"signature": "function getAttributeListValues(list: IkasProductAttributeList): IkasProductAttributeMap[]",
|
|
198
|
-
"description": "Retrieves grouped attribute values for a product attribute list, combining variant and product-level attributes.",
|
|
198
|
+
"description": "Retrieves grouped attribute values for a product attribute list, combining variant and product-level attributes.\nThis is how a PRODUCT_ATTRIBUTE_LIST code-component prop is consumed: that prop hands you a single\nIkasProductAttributeList wrapper ({ product, attributeListPropValue }) — never an array — so mapping over the\nprop is a runtime error; call this function and iterate the returned { attribute, values } maps instead.",
|
|
199
199
|
"params": [
|
|
200
200
|
{
|
|
201
201
|
"name": "list",
|
|
@@ -4628,7 +4628,7 @@
|
|
|
4628
4628
|
"name": "getIkasProductAttrributeProductIds",
|
|
4629
4629
|
"displayName": "getProductIds",
|
|
4630
4630
|
"signature": "function getIkasProductAttrributeProductIds(value: IkasProductAttributeValue): string[]",
|
|
4631
|
-
"description": "Extracts and returns the product IDs from a PRODUCT-type attribute value's JSON string.",
|
|
4631
|
+
"description": "Extracts and returns the product IDs from a PRODUCT-type attribute value's JSON string.\nNOTE: the export name really is misspelled — \"Attrribute\" with a double r. It is kept as-is because\nrenaming a published export would break every theme that imports it, so import it VERBATIM; the\ncorrectly spelled `getIkasProductAttributeProductIds` does not exist and will not compile. This is\nnot a documentation typo to report or \"fix\". In the blueprint editor the display alias is\n`getProductIds`, which hides the misspelling; only direct TypeScript imports see it.",
|
|
4632
4632
|
"params": [
|
|
4633
4633
|
{
|
|
4634
4634
|
"name": "value",
|
|
@@ -7597,14 +7597,14 @@
|
|
|
7597
7597
|
"name": "getProductVariantAppliedCampaignAmount",
|
|
7598
7598
|
"displayName": "getAppliedCampaignAmount",
|
|
7599
7599
|
"signature": "function getProductVariantAppliedCampaignAmount(variant: IkasProductVariant): IkasAppliedCampaignAmount | null",
|
|
7600
|
-
"description": "Get the applied campaign discount amount details for a product variant, including type and formatted value.",
|
|
7600
|
+
"description": "Get the applied campaign discount amount details for a product variant, including type and formatted value.\n\nHow the discount value is interpreted depends on `campaign.type`:\n- `\"FIXED_AMOUNT\"` → the value is an absolute money amount, returned in `formattedPrice` (e.g. \"$10.00\"); `discountPercentage` stays null.\n- `\"RATIO\"` → the value is a percentage, returned in `discountPercentage` (e.g. \"10\" for 10% off); `formattedPrice` stays null.\n\nBoth fixed and tiered campaigns are supported. For a tiered-discount campaign\n(`campaign.fixedDiscount` is null and the discount lives in `campaign.tieredDiscount.rules`),\nthe first tier's amount (`campaign.tieredDiscount.rules[0].amount`) is used, matching the storefront runtime.",
|
|
7601
7601
|
"params": [
|
|
7602
7602
|
{
|
|
7603
7603
|
"name": "variant",
|
|
7604
7604
|
"description": "- The product variant to check for applied campaigns"
|
|
7605
7605
|
}
|
|
7606
7606
|
],
|
|
7607
|
-
"returns": "An object
|
|
7607
|
+
"returns": "An object `{ type, formattedPrice, discountPercentage }` — `formattedPrice` is set for FIXED_AMOUNT campaigns, `discountPercentage` for RATIO campaigns (fixed and tiered alike); the other field stays null. Returns null if no campaign is applied.",
|
|
7608
7608
|
"returnType": "IkasAppliedCampaignAmount | null",
|
|
7609
7609
|
"example": "```typescript\nimport { getProductVariantAppliedCampaignAmount, getSelectedProductVariant } from \"@ikas/bp-storefront\";\nimport { IkasProduct } from \"@ikas/bp-storefront\";\n\nfunction CampaignInfo({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const campaignAmount = getProductVariantAppliedCampaignAmount(variant);\n\n if (!campaignAmount) return null;\n\n return (\n <div>\n {campaignAmount.type === \"RATIO\"\n ? `${campaignAmount.discountPercentage}% off`\n : `${campaignAmount.formattedPrice} off`}\n </div>\n );\n}\n```",
|
|
7610
7610
|
"categories": [
|
|
@@ -7999,6 +7999,36 @@
|
|
|
7999
7999
|
"IkasProductVariant"
|
|
8000
8000
|
]
|
|
8001
8001
|
},
|
|
8002
|
+
{
|
|
8003
|
+
"name": "getProductVariantTieredDiscountProducts",
|
|
8004
|
+
"displayName": "getTieredDiscountProducts",
|
|
8005
|
+
"signature": "function getProductVariantTieredDiscountProducts(variant: IkasProductVariant): IkasTieredDiscountProduct[]",
|
|
8006
|
+
"description": "Get the tiered-discount breakdown for a product variant, one entry per discount tier.\n\nA tiered discount defines multiple quantity tiers (`campaign.tieredDiscount.rules`), each with its own\namount and quantity range. This resolves every tier against the variant's price and returns the\nper-tier sell/final prices (both raw numbers and formatted strings), matching the storefront runtime.\nThe raw rules are `IkasProductTieredDiscountRule` (ranges may be null); this function returns the\nresolved `IkasTieredDiscountProduct` with normalized `{ min, max }` — use the resolved shape, not the raw rule.\n\n`sellPrice`/`finalPrice` (and their `formatted*` variants) are PER UNIT; the `*WithQuantity` fields are the\ntier total for buying the minimum quantity. A `\"FIXED_AMOUNT\"` discount is applied only to that total, so\nper-unit `finalPrice` equals `sellPrice` and the saving shows only in `finalPriceWithQuantity`.\n\nThe discount `amount` is interpreted by `campaign.type`: `\"RATIO\"` treats it as a percentage,\n`\"FIXED_AMOUNT\"` as an absolute money amount subtracted from the tier total. `amount`/`type` are\ninformational — the discount is already applied in the resolved `*Price` fields, so prefer those over\nrecomputing it yourself.\n\n`lineItemQuantityRange.min`/`max` are normalized numbers where `0` means \"unbounded\". In particular\n`max === 0` marks an open-ended top tier (e.g. \"6+\" with no upper limit) — render it as \"min+\" rather\nthan \"min–max\", and never treat `max` as a real quantity when it is `0`.\n\nEach tier's `salesChannelIds` lists the channels it targets (empty/null = all channels). Filter tiers\nagainst `IkasStorefrontConfig.salesChannelId` so only those valid for the current storefront are shown.\n\nReturns an empty array when the variant has no tiered-discount campaign, when a\n`PRODUCT_AND_VARIANT` filter excludes this variant, or when no tier defines a quantity range.",
|
|
8007
|
+
"params": [
|
|
8008
|
+
{
|
|
8009
|
+
"name": "variant",
|
|
8010
|
+
"description": "- The product variant to resolve tiered discounts for"
|
|
8011
|
+
}
|
|
8012
|
+
],
|
|
8013
|
+
"returns": "An array of tier objects (`amount`, `lineItemQuantityRange`, `type`, sell/final prices with their formatted variants, `salesChannelIds`), or an empty array if no tiered discount applies",
|
|
8014
|
+
"returnType": "IkasTieredDiscountProduct[]",
|
|
8015
|
+
"example": "```typescript\nimport {\n getProductVariantTieredDiscountProducts,\n getSelectedProductVariant,\n IkasStorefrontConfig,\n} from \"@ikas/bp-storefront\";\nimport { IkasProduct } from \"@ikas/bp-storefront\";\n\nfunction TieredDiscountTable({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const tiers = getProductVariantTieredDiscountProducts(variant);\n const salesChannelId = IkasStorefrontConfig.salesChannelId;\n\n if (!tiers.length) return null;\n\n return (\n <ul>\n {tiers.map((tier, i) => {\n // A tier applies when it targets no channels or includes the current one\n const scoped =\n !tier.salesChannelIds?.length ||\n (!!salesChannelId && tier.salesChannelIds.includes(salesChannelId));\n if (!scoped) return null;\n\n return (\n <li key={i}>\n Buy {tier.lineItemQuantityRange.min}+ → {tier.formattedFinalPriceWithQuantity}{\" \"}\n <s>{tier.formattedSellPriceWithQuantity}</s>\n </li>\n );\n })}\n </ul>\n );\n}\n```",
|
|
8016
|
+
"categories": [
|
|
8017
|
+
"Pricing",
|
|
8018
|
+
"ProductVariant"
|
|
8019
|
+
],
|
|
8020
|
+
"related": [
|
|
8021
|
+
"getProductVariantAppliedCampaignAmount",
|
|
8022
|
+
"getProductVariantCampaignOffersDiscountPercentage",
|
|
8023
|
+
"getProductVariantFinalPrice"
|
|
8024
|
+
],
|
|
8025
|
+
"isAsync": false,
|
|
8026
|
+
"isClass": false,
|
|
8027
|
+
"parameterTypes": [
|
|
8028
|
+
"IkasProductVariant",
|
|
8029
|
+
"IkasTieredDiscountProduct"
|
|
8030
|
+
]
|
|
8031
|
+
},
|
|
8002
8032
|
{
|
|
8003
8033
|
"name": "getProductVariantIsBackInStockEnabled",
|
|
8004
8034
|
"displayName": "isBackInStockEnabled",
|
|
@@ -9556,7 +9586,7 @@
|
|
|
9556
9586
|
"name": "initAddressForm",
|
|
9557
9587
|
"displayName": "init",
|
|
9558
9588
|
"signature": "async function initAddressForm(addressForm: AddressForm, address?: IkasCustomerAddress): Promise<void>",
|
|
9559
|
-
"description": "Initializes an address form with default or existing address values, loads location data, and configures field settings.",
|
|
9589
|
+
"description": "Initializes an address form with default or existing address values, loads location data, and configures field settings.\n\nAlso resolves `addressForm.addressFormat` for the selected country. That is an\n`AddressFormItem[][]` describing which fields the country uses and how they group into rows\n(outer array = rows, inner array = fields sharing a row), and it is the only correct source\nfor laying out an address form -- a hardcoded field list will be wrong for most countries.\nIt is empty until this promise resolves, and `setAddressFormCountry` re-resolves it, so\nrender from it rather than copying it into local state.",
|
|
9560
9590
|
"params": [
|
|
9561
9591
|
{
|
|
9562
9592
|
"name": "addressForm",
|
|
@@ -9569,7 +9599,7 @@
|
|
|
9569
9599
|
],
|
|
9570
9600
|
"returns": "A promise that resolves once the form is fully initialized, including country/state/city lists, field settings, checkout settings, and shipping zones.",
|
|
9571
9601
|
"returnType": "Promise<void>",
|
|
9572
|
-
"example": "```
|
|
9602
|
+
"example": "```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n type AddressForm,\n type AddressFormItem,\n initAddressForm,\n setAddressFormFirstName,\n setAddressFormLastName,\n setAddressFormIdentityNumber,\n setAddressFormAddressLine1,\n setAddressFormAddressLine2,\n setAddressFormPostalCode,\n setAddressFormPhone,\n} from \"@ikas/bp-storefront\";\n\n// Initialize an empty address form\nawait initAddressForm(addressForm);\n\n// Initialize with an existing customer address for editing\nawait initAddressForm(addressForm, existingAddress);\n\n// addressFormat is empty until init resolves, so gate the render on it.\nconst [isLoading, setIsLoading] = useState(true);\nuseEffect(() => {\n initAddressForm(addressForm, address).then(() => setIsLoading(false));\n}, []);\n\n// Every addressFormat key has a matching setter, each taking (addressForm, value).\n// The location keys (country, state, city, district, region) are omitted here: they\n// render as selects fed by addressForm.countryOptions / stateOptions / cityOptions /\n// districtOptions / regionOptions, and must fall back to a text input when the field\n// isFreeText or has no options.\nconst SETTERS: Partial<Record<AddressFormItem, (form: AddressForm, value: string) => void>> = {\n firstName: setAddressFormFirstName,\n lastName: setAddressFormLastName,\n identityNumber: setAddressFormIdentityNumber,\n addressLine1: setAddressFormAddressLine1,\n addressLine2: setAddressFormAddressLine2,\n postalCode: setAddressFormPostalCode,\n phone: setAddressFormPhone,\n};\n\n// Render the country's own layout: outer array = rows, inner array = fields in that row.\n(addressForm.addressFormat ?? []).map((row) => {\n // A field the country does not use is absent from the form object entirely.\n const keys = row.filter((key) => addressForm[key] != null);\n if (!keys.length) return null;\n\n return (\n <div className=\"address-row\" key={keys.join(\"-\")}>\n {keys.map((key) => {\n // Each entry is an IkasFormItem: value, label, placeholder, hasError, message.\n const field = addressForm[key];\n if (!field) return null; // already guaranteed by the filter above; narrows the type\n return (\n <label key={key}>\n {field.label}\n <input\n value={field.value ?? \"\"}\n placeholder={field.placeholder}\n onInput={(e) => SETTERS[key]?.(addressForm, e.currentTarget.value)}\n />\n {field.hasError && <small>{field.message}</small>}\n </label>\n );\n })}\n </div>\n );\n})\n```",
|
|
9573
9603
|
"categories": [
|
|
9574
9604
|
"Address",
|
|
9575
9605
|
"Form",
|
|
@@ -14862,7 +14892,7 @@
|
|
|
14862
14892
|
{
|
|
14863
14893
|
"name": "getEmptyAddressForm",
|
|
14864
14894
|
"signature": "function getEmptyAddressForm(customerStore: IkasCustomerStore): AddressForm",
|
|
14865
|
-
"description": "Create and return a new, empty address form with validation, wrapped in MobX observability.",
|
|
14895
|
+
"description": "Create and return a new, empty address form with validation, wrapped in MobX observability.\n\nUse this to create an address; use `getIkasCustomerAddressForm(address)` to edit an existing\none. It starts `initAddressForm` internally but does not await it, so await `initAddressForm`\nyourself when you need to know that the country layout (`addressFormat`) and the country/state/\ncity lists are ready to render.",
|
|
14866
14896
|
"params": [
|
|
14867
14897
|
{
|
|
14868
14898
|
"name": "customerStore",
|
|
@@ -14871,14 +14901,16 @@
|
|
|
14871
14901
|
],
|
|
14872
14902
|
"returns": "A new observable address form object with validated fields",
|
|
14873
14903
|
"returnType": "AddressForm",
|
|
14874
|
-
"example": "```
|
|
14904
|
+
"example": "```tsx\nimport {\n customerStore,\n getEmptyAddressForm,\n initAddressForm,\n setAddressFormAddressLine1,\n submitAddressForm,\n} from \"@ikas/bp-storefront\";\n\n// Create the form once, then await init before rendering the country-driven layout.\nconst [form] = useState(() => getEmptyAddressForm(customerStore));\nconst [isLoading, setIsLoading] = useState(true);\nuseEffect(() => {\n initAddressForm(form).then(() => setIsLoading(false));\n}, []);\n\n// Always go through the setters. Assigning `form.addressLine1.value` directly skips\n// re-validation and the dependent-field resets the location setters perform.\nsetAddressFormAddressLine1(form, \"123 Main Street\");\n\n// submitAddressForm validates and saves the address onto the customer itself --\n// do not hand-build a customer object and call saveCustomer for this.\nconst ok = await submitAddressForm(form);\n```",
|
|
14875
14905
|
"categories": [
|
|
14876
14906
|
"Customer",
|
|
14877
14907
|
"Address"
|
|
14878
14908
|
],
|
|
14879
14909
|
"related": [
|
|
14880
|
-
"
|
|
14881
|
-
"
|
|
14910
|
+
"initAddressForm",
|
|
14911
|
+
"submitAddressForm",
|
|
14912
|
+
"getIkasCustomerAddressForm",
|
|
14913
|
+
"deleteCustomerAddress"
|
|
14882
14914
|
],
|
|
14883
14915
|
"isAsync": false,
|
|
14884
14916
|
"isClass": false,
|
|
@@ -15296,6 +15328,44 @@
|
|
|
15296
15328
|
"isAsync": false,
|
|
15297
15329
|
"isClass": false
|
|
15298
15330
|
},
|
|
15331
|
+
{
|
|
15332
|
+
"name": "PayWithIkas",
|
|
15333
|
+
"signature": "function PayWithIkas({ product, quantity = 1, className, style }: PayWithIkasProps): VNode<ClassAttributes<HTMLIFrameElement> & { ref: MutableRef<HTMLIFrameElement | null>; title: string; ... 4 more ...; style: { ...; }; }> | null",
|
|
15334
|
+
"description": "\"Pay with ikas\" (Hızlı Öde) fast-checkout button for the product detail page.\nDrop-in Preact component that renders the pay-with-ikas checkout iframe and\nwires up its whole message protocol, so a theme gets the one-click \"Hızlı Öde\"\nflow without owning any of the plumbing. Use it next to the add-to-cart button\non the product detail page.\n\nThe pay-with-ikas URL is read from `IkasStorefrontConfig.getPayWithIkasUrl()`;\nwhen the merchant has not enabled Pay with ikas the component renders nothing.\nThe add-to-cart-enabled state is tracked reactively via mobx `autorun`, so the\niframe stays in sync as the product's availability changes (e.g. variant\nselection). Clicking \"Hızlı Öde\" creates a temporary cart via\n`createPayWithIkasSession` and redirects to the returned pay URL.",
|
|
15335
|
+
"params": [
|
|
15336
|
+
{
|
|
15337
|
+
"name": "product",
|
|
15338
|
+
"description": "- The product to fast-checkout via Pay with ikas"
|
|
15339
|
+
},
|
|
15340
|
+
{
|
|
15341
|
+
"name": "quantity",
|
|
15342
|
+
"description": "- Quantity to purchase (defaults to 1)"
|
|
15343
|
+
},
|
|
15344
|
+
{
|
|
15345
|
+
"name": "className",
|
|
15346
|
+
"description": "- Optional CSS class applied to the iframe"
|
|
15347
|
+
},
|
|
15348
|
+
{
|
|
15349
|
+
"name": "style",
|
|
15350
|
+
"description": "- Optional inline styles applied to the iframe"
|
|
15351
|
+
}
|
|
15352
|
+
],
|
|
15353
|
+
"returnType": "VNode<ClassAttributes<HTMLIFrameElement> & { ref: MutableRef<HTMLIFrameElement | null>; title: string; ... 4 more ...; style: { ...; }; }> | null",
|
|
15354
|
+
"example": "```tsx\nimport { PayWithIkas } from \"@ikas/bp-storefront\";\n\n// On the product detail page, alongside the add-to-cart button:\n<PayWithIkas product={product} quantity={quantity} />\n```",
|
|
15355
|
+
"categories": [
|
|
15356
|
+
"ProductDetail",
|
|
15357
|
+
"Checkout"
|
|
15358
|
+
],
|
|
15359
|
+
"related": [
|
|
15360
|
+
"createPayWithIkasSession",
|
|
15361
|
+
"isAddToCartEnabled"
|
|
15362
|
+
],
|
|
15363
|
+
"isAsync": false,
|
|
15364
|
+
"isClass": false,
|
|
15365
|
+
"parameterTypes": [
|
|
15366
|
+
"IkasProps"
|
|
15367
|
+
]
|
|
15368
|
+
},
|
|
15299
15369
|
{
|
|
15300
15370
|
"name": "normalizeSvg",
|
|
15301
15371
|
"signature": "function normalizeSvg(svg: string, options: NormalizeSvgOptions): string",
|