@ikas/code-components-mcp 1.4.0-beta.23 → 1.4.0-beta.230
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 +191 -78
- package/data/migration.json +10 -4
- package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +13 -0
- package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +1 -1
- package/data/section-templates/account-info-section/ikas-config-snippet.json +19 -0
- 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 +15 -0
- 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 +21 -0
- 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 +13 -0
- package/data/section-templates/favorites/ikas-config-snippet.json +18 -1
- package/data/section-templates/features-section/ikas-config-snippet.json +13 -0
- package/data/section-templates/footer-section/ikas-config-snippet.json +15 -0
- 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 +7 -0
- 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 +27 -0
- package/data/section-templates/header-section/children/Navbar/index.tsx +6 -5
- package/data/section-templates/header-section/ikas-config-snippet.json +13 -0
- 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 +13 -0
- package/data/section-templates/image-handling/ikas-config-snippet.json +53 -0
- 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 +27 -0
- 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 +27 -0
- package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +29 -0
- 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 +53 -0
- 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 +17 -0
- 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 +1046 -80
- package/data/storefront-types.json +9868 -6037
- package/dist/index.js +2578 -257
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/migration.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prop-type-mapping": {
|
|
18
18
|
"title": "Prop Type Mapping: Old → New",
|
|
19
19
|
"description": "Complete mapping of old theme.json prop types to new ikas.config.json prop types",
|
|
20
|
-
"content": "## IMPORTANT: Different Systems, Different Types\n\n**The old prop type system (theme.json) and the new prop type system (ikas.config.json) are completely separate.** Even when a prop type has the same name in both systems (e.g., `IMAGE`, `LINK`, `PRODUCT_LIST`), the underlying TypeScript types come from different packages (`@ikas/storefront` vs `@ikas/bp-storefront`) and may have different properties, methods, and nullability. Always use the MCP tools (`get_type_definition`, `get_model_guide`) to look up the exact new-system types — never assume old-system knowledge applies.\n\n## Direct Mappings (1:1)\n\nThese prop types have similar equivalents, but remember the TypeScript types are from different packages:\n\n| Old Type | New Type | TypeScript (Old) | TypeScript (New) | Notes |\n|----------|----------|-----------------|-----------------|-------|\n| `TEXT` | `TEXT` | `string` | `string` | |\n| `RICH_TEXT` | `RICH_TEXT` | `string` | `string` | |\n| `BOOLEAN` | `BOOLEAN` | `boolean` | `boolean` | |\n| `IMAGE` | `IMAGE` | `IkasImage` | `IkasImage \\| null` | New type is nullable |\n| `IMAGE_LIST` | `IMAGE_LIST` | `IkasImage[]` | `IkasImageList` | Slight type difference |\n| `LINK` | `LINK` | `IkasNavigationLink` | `IkasNavigationLink` | |\n| `LIST_OF_LINK` | `LIST_OF_LINK` | `IkasNavigationLink[]` | `IkasNavigationLinkList` | Slight type difference |\n| `COLOR` | `COLOR` | `string` | `string` | |\n| `VIDEO` | `VIDEO` | `IkasVideo` | `IkasVideo \\| null` | New type is nullable |\n| `PRODUCT_LIST` | `PRODUCT_LIST` | `IkasProductList` | `IkasProductList` | |\n| `CATEGORY` | `CATEGORY` | `IkasCategory` | `IkasCategory \\| null` | |\n| `CATEGORY_LIST` | `CATEGORY_LIST` | `IkasCategoryList` | `IkasCategoryList` | |\n| `BRAND` | `BRAND` | `IkasBrand` | `IkasBrand \\| null` | |\n| `BRAND_LIST` | `BRAND_LIST` | — | `IkasBrandList` | |\n| `BLOG` | `BLOG` | `IkasBlog` | `IkasBlog \\| null` | |\n| `BLOG_LIST` | `BLOG_LIST` | `IkasBlogList` | `IkasBlogList` | |\n| `BLOG_CATEGORY` | `BLOG_CATEGORY` | `IkasBlogCategory` | `IkasBlogCategory \\| null` | |\n| `BLOG_CATEGORY_LIST` | `BLOG_CATEGORY_LIST` | `IkasBlogCategoryList` | `IkasBlogCategoryList` | |\n| `PRODUCT_ATTRIBUTE` | `PRODUCT_ATTRIBUTE` | `IkasAttributeDetail` | `
|
|
20
|
+
"content": "## IMPORTANT: Different Systems, Different Types\n\n**The old prop type system (theme.json) and the new prop type system (ikas.config.json) are completely separate.** Even when a prop type has the same name in both systems (e.g., `IMAGE`, `LINK`, `PRODUCT_LIST`), the underlying TypeScript types come from different packages (`@ikas/storefront` vs `@ikas/bp-storefront`) and may have different properties, methods, and nullability. Always use the MCP tools (`get_type_definition`, `get_model_guide`) to look up the exact new-system types — never assume old-system knowledge applies.\n\n## Direct Mappings (1:1)\n\nThese prop types have similar equivalents, but remember the TypeScript types are from different packages:\n\n| Old Type | New Type | TypeScript (Old) | TypeScript (New) | Notes |\n|----------|----------|-----------------|-----------------|-------|\n| `TEXT` | `TEXT` | `string` | `string` | |\n| `RICH_TEXT` | `RICH_TEXT` | `string` | `string` | |\n| `BOOLEAN` | `BOOLEAN` | `boolean` | `boolean` | |\n| `IMAGE` | `IMAGE` | `IkasImage` | `IkasImage \\| null` | New type is nullable |\n| `IMAGE_LIST` | `IMAGE_LIST` | `IkasImage[]` | `IkasImageList` | Slight type difference |\n| `LINK` | `LINK` | `IkasNavigationLink` | `IkasNavigationLink` | |\n| `LIST_OF_LINK` | `LIST_OF_LINK` | `IkasNavigationLink[]` | `IkasNavigationLinkList` | Slight type difference |\n| `COLOR` | `COLOR` | `string` | `string` | |\n| `VIDEO` | `VIDEO` | `IkasVideo` | `IkasVideo \\| null` | New type is nullable |\n| `PRODUCT_LIST` | `PRODUCT_LIST` | `IkasProductList` | `IkasProductList` | |\n| `CATEGORY` | `CATEGORY` | `IkasCategory` | `IkasCategory \\| null` | |\n| `CATEGORY_LIST` | `CATEGORY_LIST` | `IkasCategoryList` | `IkasCategoryList` | |\n| `BRAND` | `BRAND` | `IkasBrand` | `IkasBrand \\| null` | |\n| `BRAND_LIST` | `BRAND_LIST` | — | `IkasBrandList` | |\n| `BLOG` | `BLOG` | `IkasBlog` | `IkasBlog \\| null` | |\n| `BLOG_LIST` | `BLOG_LIST` | `IkasBlogList` | `IkasBlogList` | |\n| `BLOG_CATEGORY` | `BLOG_CATEGORY` | `IkasBlogCategory` | `IkasBlogCategory \\| null` | |\n| `BLOG_CATEGORY_LIST` | `BLOG_CATEGORY_LIST` | `IkasBlogCategoryList` | `IkasBlogCategoryList` | |\n| `PRODUCT_ATTRIBUTE` | `PRODUCT_ATTRIBUTE` | `IkasAttributeDetail` | `IkasProductAttributeDetail \\| null` | Renamed only — same wrapper shape `{ product, attributePropValue }`; read values with `getAttributeDetailValues()` |\n| `PRODUCT_ATTRIBUTE_LIST` | `PRODUCT_ATTRIBUTE_LIST` | `IkasAttributeList` | `IkasProductAttributeList` | Renamed only — still a single wrapper object (NOT an array); read values with `getAttributeListValues()` |\n| `COMPONENT_LIST` | `COMPONENT_LIST` | `IkasComponentRenderer` | `any` (rendered via `<IkasComponentRenderer>`) | |\n\n## Renamed Types\n\n| Old Type | New Type | Notes |\n|----------|----------|-------|\n| `PRODUCT_DETAIL` | `PRODUCT` | Same concept, just renamed. Old: `IkasProduct`, New: `IkasProduct \\| null` |\n\n## Types Requiring Conversion\n\n### SLIDER → NUMBER\nOld `SLIDER` type used `IkasSlider` with `{ value: number }`. The new system uses `NUMBER` which maps to plain `number`.\n\n**Old theme.json:**\n```json\n{\n \"name\": \"logoWidth\",\n \"type\": \"SLIDER\",\n \"sliderData\": { \"min\": 80, \"max\": 300, \"interval\": 1 }\n}\n```\n\n**New ikas.config.json:**\n```json\n{\n \"name\": \"logoWidth\",\n \"displayName\": \"Logo Width\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"defaultValue\": 120\n}\n```\n\n**Code change:** Replace `logoWidth?.value` or `logoWidth.value` with just `logoWidth` (it's already a number).\n\n### CUSTOM → Multiple approaches\nSee `get_migration_guide(\"custom-data-conversion\")` for the full guide. Summary:\n\n| Old customData type | New approach |\n|--------------------|--------------|\n| DYNAMIC_LIST (list of objects) | `COMPONENT_LIST` + child component |\n| OBJECT (≤3 simple fields) | Flatten into direct props on parent |\n| OBJECT (>3 fields or complex) | `COMPONENT_LIST` + child component |\n| STATIC_LIST (fixed count items) | `COMPONENT_LIST` (store owner manages count) |\n| ENUM (enumOptions) | `ENUM` with custom enum via `config add-enum` |\n\n### ENUM → ENUM\nOld ENUM type with `enumOptions` maps to new ENUM with `enumTypeId`. Create custom enums first:\n```bash\nnpx ikas-component config add-enum --name \"AspectRatio\" --options '{\"Square\":\"1:1\",\"Landscape\":\"16:9\",\"Portrait\":\"3:4\"}'\n# Use returned enumId in prop definition\n```\n\n## Types Not Available in New System\n\nThese old types do not exist in the new system and need workarounds:\n\n| Old Type | Workaround |\n|----------|------------|\n| `SLIDER` | Use `NUMBER`. Access value directly instead of `.value` |\n| `OBJECT` (customData) | Flatten into parent props or use COMPONENT_LIST |\n| `STATIC_LIST` (customData) | Use COMPONENT_LIST |\n\n## New Types Not in Old System\n\n| New Type | Description |\n|----------|-------------|\n| `NUMBER` | Numeric input (replaces SLIDER) |\n| `NUMBER_RANGE` | Two-number range (IkasNumberRange) |\n| `DATE` | Date picker |\n| `FUNCTION` | Function prop (callback) |\n| `TYPE` | Structured style type (padding, margin, etc.) |\n| `COMPONENT` | Single child component slot |\n| `RAFFLE` / `RAFFLE_LIST` | Raffle references |",
|
|
21
21
|
"tags": [
|
|
22
22
|
"migration",
|
|
23
23
|
"props",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"storefront-import-mapping": {
|
|
89
89
|
"title": "Import Mapping: @ikas/storefront → @ikas/bp-storefront",
|
|
90
90
|
"description": "Maps old @ikas/storefront imports to new @ikas/bp-storefront equivalents",
|
|
91
|
-
"content": "## Old Package → New Package\n\nAll imports change from `@ikas/storefront` to `@ikas/bp-storefront`. **This is NOT a find-and-replace.** Even types with the same name (`IkasImage`, `IkasProduct`, etc.) come from different packages and may have different shapes. Always verify with `get_type_definition` or `get_model_guide` before assuming compatibility.\n\n## Type Rename Table\n\n| Old Import (`@ikas/storefront`) | New Import (`@ikas/bp-storefront`) | Notes |\n|-----------|-----------|-------|\n| `IkasImage`, `IkasVideo`, `IkasNavigationLink`, `IkasProduct`, `IkasProductList`, `IkasCategory`, `IkasCategoryList`, `IkasBrand`, `IkasBlog`, `IkasBlogList`, `IkasBlogCategory`, `IkasBlogCategoryList` | Same name | Different package — shape may differ; verify before assuming. |\n| `IkasSlider` | *(removed)* | Use plain `number`. |\n| `IkasAttributeDetail` | `
|
|
91
|
+
"content": "## Old Package → New Package\n\nAll imports change from `@ikas/storefront` to `@ikas/bp-storefront`. **This is NOT a find-and-replace.** Even types with the same name (`IkasImage`, `IkasProduct`, etc.) come from different packages and may have different shapes. Always verify with `get_type_definition` or `get_model_guide` before assuming compatibility.\n\n## Type Rename Table\n\n| Old Import (`@ikas/storefront`) | New Import (`@ikas/bp-storefront`) | Notes |\n|-----------|-----------|-------|\n| `IkasImage`, `IkasVideo`, `IkasNavigationLink`, `IkasProduct`, `IkasProductList`, `IkasCategory`, `IkasCategoryList`, `IkasBrand`, `IkasBlog`, `IkasBlogList`, `IkasBlogCategory`, `IkasBlogCategoryList` | Same name | Different package — shape may differ; verify before assuming. |\n| `IkasSlider` | *(removed)* | Use plain `number`. |\n| `IkasAttributeDetail` | `IkasProductAttributeDetail` | Renamed only; still the `{ product, attributePropValue }` wrapper. `IkasProductAttributeValue` is a DIFFERENT type — the resolved value returned by `getAttributeDetailValues()`. |\n| `IkasAttributeList` | `IkasProductAttributeList` | Renamed only; still a single `{ product, attributeListPropValue }` wrapper, NOT an array. Values come from `getAttributeListValues()`. |\n| `IkasComponentRenderer` | `IkasComponentRenderer` | Same name, different usage — see below. |\n\n## Function / Pattern Rename Table\n\n| Old | New | Notes |\n|-----|-----|-------|\n| `<Image>` component | `getDefaultSrc()` / `getSrc()` + native `<img>` | See `react-to-preact` for image patterns. |\n| `useStore()` hook | Direct imports (`cartStore`, `customerStore`, ...) | Stores are MobX observables; no wrapper hook. |\n| `<IkasComponentRenderer>{children}</...>` | `<IkasComponentRenderer id=\"...\" components={list as any[]} parentProps={props} />` | New props-based API. |\n\nFor all import/observer/hook/event-handling behavior changes, see `get_migration_guide(\"react-to-preact\")` — this topic is intentionally a quick rename reference.\n",
|
|
92
92
|
"tags": [
|
|
93
93
|
"migration",
|
|
94
94
|
"imports",
|
|
@@ -212,13 +212,19 @@
|
|
|
212
212
|
"prop-runtime-shapes": {
|
|
213
213
|
"title": "Runtime Shapes of Prop Types",
|
|
214
214
|
"description": "Exact TypeScript shapes your component receives at runtime for each prop type. Covers common confusions (.data vs .links, .variant vs .product).",
|
|
215
|
-
"content": "When you declare a prop in `ikas.config.json`, the component receives a specific runtime shape. This table documents what you actually get — which is often different from the editor UI suggests and from naive expectations.\n\n## Reference Table\n\n| PropType | Runtime Shape | Key Access Pattern |\n|----------|---------------|--------------------|\n| `TEXT` | `string` | `props.myText` |\n| `RICH_TEXT` | `string` (HTML) | `<div dangerouslySetInnerHTML={{ __html: props.html }} />` |\n| `NUMBER` | `number` | `props.width` (direct number, NOT `.value`) |\n| `BOOLEAN` | `boolean` | `props.enabled` |\n| `COLOR` | `string` (CSS color) | `style={{ color: props.textColor }}` |\n| `IMAGE` | `IkasImage \\| null` | `getDefaultSrc(props.image)` or `getSrc(props.image, { width: 400 })` |\n| `IMAGE_LIST` | `IkasImageList` | `props.images.map(img => getDefaultSrc(img))` |\n| `VIDEO` | `IkasVideo \\| null` | Check `.url`, `.type` (EMBED vs FILE) |\n| `LINK` | `IkasNavigationLink` | `props.link.href`, `props.link.label`, `props.link.subLinks`, `props.link.openInNewTab` |\n| `LIST_OF_LINK` | `IkasNavigationLinkList` | **`props.links.links`** — access via `.links` property (NOT `.data`) |\n| `PRODUCT` | `IkasProduct \\| null` | `props.product.name`, `.prices`, etc. |\n| `PRODUCT_LIST` | `IkasProductList` | **`props.productList.data`** — array of products via `.data` property |\n| `CATEGORY` | `IkasCategory \\| null` | `props.category.name`, `.href` |\n| `CATEGORY_LIST` | `IkasCategoryList` | Check shape with `get_type_definition(\"IkasCategoryList\")` |\n| `BRAND` / `BRAND_LIST` | `IkasBrand \\| null` / `IkasBrandList` | Use `get_model_guide(\"IkasBrand\")` |\n| `BLOG` / `BLOG_LIST` | `IkasBlog \\| null` / `IkasBlogList` | Blog list uses `.data` |\n| `PRODUCT_ATTRIBUTE` | `
|
|
215
|
+
"content": "When you declare a prop in `ikas.config.json`, the component receives a specific runtime shape. This table documents what you actually get — which is often different from the editor UI suggests and from naive expectations.\n\n## Reference Table\n\n| PropType | Runtime Shape | Key Access Pattern |\n|----------|---------------|--------------------|\n| `TEXT` | `string` | `props.myText` |\n| `RICH_TEXT` | `string` (HTML) | `<div dangerouslySetInnerHTML={{ __html: props.html }} />` |\n| `NUMBER` | `number` | `props.width` (direct number, NOT `.value`) |\n| `BOOLEAN` | `boolean` | `props.enabled` |\n| `COLOR` | `string` (CSS color) | `style={{ color: props.textColor }}` |\n| `IMAGE` | `IkasImage \\| null` | `getDefaultSrc(props.image)` or `getSrc(props.image, { width: 400 })` |\n| `IMAGE_LIST` | `IkasImageList` | `props.images.map(img => getDefaultSrc(img))` |\n| `VIDEO` | `IkasVideo \\| null` | Check `.url`, `.type` (EMBED vs FILE) |\n| `LINK` | `IkasNavigationLink` | `props.link.href`, `props.link.label`, `props.link.subLinks`, `props.link.openInNewTab` |\n| `LIST_OF_LINK` | `IkasNavigationLinkList` | **`props.links.links`** — access via `.links` property (NOT `.data`) |\n| `PRODUCT` | `IkasProduct \\| null` | `props.product.name`, `.prices`, etc. |\n| `PRODUCT_LIST` | `IkasProductList` | **`props.productList.data`** — array of products via `.data` property |\n| `CATEGORY` | `IkasCategory \\| null` | `props.category.name`, `.href` |\n| `CATEGORY_LIST` | `IkasCategoryList` | Check shape with `get_type_definition(\"IkasCategoryList\")` |\n| `BRAND` / `BRAND_LIST` | `IkasBrand \\| null` / `IkasBrandList` | Use `get_model_guide(\"IkasBrand\")` |\n| `BLOG` / `BLOG_LIST` | `IkasBlog \\| null` / `IkasBlogList` | Blog list uses `.data` |\n| `PRODUCT_ATTRIBUTE` | `IkasProductAttributeDetail \\| null` | Wrapper `{ product, attributePropValue }` — **NOT** the value; `getAttributeDetailValues(prop)` |\n| `PRODUCT_ATTRIBUTE_LIST` | `IkasProductAttributeList` | Wrapper `{ product, attributeListPropValue }` — a single object, **NOT** an array; `getAttributeListValues(prop)` |\n| `ENUM` | `string` (the enum value) | Compare directly: `props.type === \"email\"` |\n| `COMPONENT` | `any` | Pass to `<IkasComponentRenderer components={[comp] as any[]} />` |\n| `COMPONENT_LIST` | `any[]` | Pass to `<IkasComponentRenderer components={list as any[]} />` |\n\n## Authored `defaultValue` ≠ runtime read shape (LINK / LIST_OF_LINK)\n\nThe shapes above are what your component RECEIVES (read access: `props.link.href`). They are NOT what you write as a prop `defaultValue` in `ikas.config.json`. The authored default is a typed object:\n- `LINK` → `{ \"linkType\": \"EXTERNAL\", \"label\": \"Shop\", \"externalLink\": \"https://…\", \"subLinks\": [] }` (or `\"linkType\": \"PAGE\"` with `\"pageType\"`).\n- `LIST_OF_LINK` → `{ \"links\": [ <link>, … ] }`.\n\nDo NOT author a default as `{ href, label }` (legacy shape) or as a JSON string — the CLI rejects both. Full reference: `get_framework_guide(\"prop-types\")` → \"LINK and LIST_OF_LINK props\".\n\n## Three shapes, one prop (PRODUCT_ATTRIBUTE / PRODUCT_ATTRIBUTE_LIST)\n\nSame trap as LINK, but worse: the prop you receive is a **wrapper**, not a value, and accessing the value you expect fails silently — `check`/`build` pass, every field is `undefined`, the component renders nothing. The editor stores `{ attributeId, usePageData }`, your component receives the `{ product, attributePropValue }` wrapper, and the values only come out of the helpers:\n\n```tsx\nimport { getAttributeDetailValues, getAttributeListValues } from \"@ikas/bp-storefront\";\n\n// WRONG — no such field at runtime; compiles clean, renders nothing\nreturn attribute?.value ?? null;\n\n// RIGHT — PRODUCT_ATTRIBUTE\nif (!attribute?.attributePropValue?.attributeId) return null;\nconst html = getAttributeDetailValues(attribute).map((v) => v.value).filter(Boolean).join(\"\");\n\n// RIGHT — PRODUCT_ATTRIBUTE_LIST (a single object, never `.map` it directly)\nconst groups = getAttributeListValues(attributeList); // IkasProductAttributeMap[]: { attribute, values }\n```\n\nFull reference: `get_framework_guide(\"prop-types\")` → \"PRODUCT_ATTRIBUTE and PRODUCT_ATTRIBUTE_LIST props\".\n\n## Critical: Inconsistent Access Patterns\n\nThe most common mistakes come from **list-type props having inconsistent access patterns**:\n\n- `PRODUCT_LIST` → `.data` (array of products)\n- `BLOG_LIST` → `.data` (array of blogs)\n- `LIST_OF_LINK` → `.links` (NOT `.data`)\n- `PRODUCT_ATTRIBUTE_LIST` → a single wrapper object (NOT an array): `.attributeListPropValue.attributeIds` holds the selection; values come from `getAttributeListValues(prop)` (grouped `{ attribute, values }` maps)\n- `PRODUCT_ATTRIBUTE` → a single wrapper object too: `.attributePropValue.attributeId`; values come from `getAttributeDetailValues(prop)`\n- `IMAGE_LIST` → plain array via `.data` (check with `get_type_definition`)\n\n**When in doubt, call `get_model_guide(\"IkasProductList\")` etc. for the exact shape.**\n\n## Image Sizing: getDefaultSrc vs getSrc\n\n- `getDefaultSrc(image)` — returns the default-sized URL. Use for small images, thumbnails, or when size doesn't matter.\n- `getSrc(image, { width: 400 })` — returns a URL at a specific width. Use for performance-critical images where you know the rendered size.\n- `createMediaSrcset(image)` — generates a responsive `srcset` string for `<img srcset={...}>`.\n\n**Rule of thumb:** Use `getSrc` with a concrete width for large images; use `getDefaultSrc` for icons/small images.\n\n## Store Data Shapes (cartStore, customerStore, etc.)\n\nStore data is NOT a prop type — you import stores directly from `@ikas/bp-storefront`. Their runtime shapes are:\n\n- `cartStore.cart` → `IkasCart`: `.orderLineItems` is the array; each item has **`.variant`** (not `.product`), `.quantity`, `.totalPrice`\n- `customerStore.customer` → `IkasCustomer | null`: null when logged out, has `.firstName`, `.lastName`, `.email`, `.id`\n- `customerStore.isAuthenticated` → boolean\n\n**For full store shapes, always use `get_model_guide(\"IkasCart\")`, `get_model_guide(\"IkasCustomer\")`, etc. — do not guess.**\n\n## Old → New Runtime Shape Changes\n\nWhen migrating, remember these runtime changes:\n\n- Old `IkasSlider` (with `.value`) → New `number` (plain). Replace `width?.value` with just `width`.\n- Old `<Image>` component (renders itself) → New `getDefaultSrc(image)` + native `<img>`.\n- Old `useStore()` hook → New direct import: `import { cartStore } from '@ikas/bp-storefront'`.\n- Old `IkasAttributeDetail` / `IkasAttributeList` → New `IkasProductAttributeDetail` / `IkasProductAttributeList`. **Rename only — the wrapper shape did NOT change.** See `get_framework_guide(\"prop-types\")` → \"PRODUCT_ATTRIBUTE and PRODUCT_ATTRIBUTE_LIST props\".\n\n\n\n## Order Line Item Shapes (cart, order pages)\n\nThese come up when iterating over `IkasCart.orderLineItems` or `IkasOrder.orderLineItems`. They trip up LLMs because the field names don't match scalar intuition:\n\n- **`IkasOrderLineItemOption.values`** — this is an **array** of `IkasOrderLineItemOptionValue`, NOT a scalar `.value`. Iterate it with `.map`:\n\n ```tsx\n // WRONG — there is no .value on IkasOrderLineItemOption\n <span>{option.value}</span>\n\n // RIGHT — .values is an array, each item has its own .name/.value/.formattedPrice\n {option.values.map((v) => (\n <span key={v.name}>{v.name}: {v.value}</span>\n ))}\n ```\n\n- **`IkasOrderAdjustment` has no `.formattedAmount` property.** Call the helper instead: `getOrderAdjustmentFormattedAmount(adjustment)` from `@ikas/bp-storefront`. The helper returns the formatted currency string and handles the sign (negated for decrements). See `get_function_doc(\"getOrderAdjustmentFormattedAmount\")`.\n\n## See Also\n- `get_model_guide(\"<TypeName>\")` — one-stop shop for type definition + utility functions + examples\n- `get_type_definition(\"<TypeName>\")` — raw type definition\n- `get_framework_guide(\"common-pitfalls\")` — runtime shape mistakes (point #9 covers `.data`)\n- `get_framework_guide(\"imports\")` — where each type/function comes from",
|
|
216
216
|
"tags": [
|
|
217
217
|
"migration",
|
|
218
218
|
"runtime",
|
|
219
219
|
"shapes",
|
|
220
220
|
"PRODUCT_LIST",
|
|
221
221
|
"LIST_OF_LINK",
|
|
222
|
+
"PRODUCT_ATTRIBUTE",
|
|
223
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
224
|
+
"IkasProductAttributeDetail",
|
|
225
|
+
"IkasProductAttributeList",
|
|
226
|
+
"getAttributeDetailValues",
|
|
227
|
+
"getAttributeListValues",
|
|
222
228
|
"IkasCart",
|
|
223
229
|
"getDefaultSrc",
|
|
224
230
|
"getSrc",
|
|
@@ -230,7 +236,7 @@
|
|
|
230
236
|
"link-prop-decision-guide": {
|
|
231
237
|
"title": "LINK Prop Decision Guide",
|
|
232
238
|
"description": "When to use LINK, LIST_OF_LINK, LINK with subLinks, or COMPONENT_LIST for link-related data. Addresses common confusion between similar prop types.",
|
|
233
|
-
"content": "Several prop types can represent \"links\" in the new system. Choosing the right one depends on the shape of the data and what the store owner should control.\n\n## Decision Tree\n\n**Is it a single link?**\n→ Use `LINK`. Runtime: `IkasNavigationLink { href, label, subLinks, openInNewTab, ... }`\n\n**Is it a flat list of sibling links (no extra data per link)?**\n→ Use `LIST_OF_LINK`. Runtime: `IkasNavigationLinkList` with `.links: IkasNavigationLink[]`\n\n**Is it a hierarchical menu (links with sub-links, no extra data)?**\n→ Use `LIST_OF_LINK`. Each `IkasNavigationLink` already has `.subLinks: IkasNavigationLink[]`. The editor handles sub-link nesting natively.\n\n**Is it a list of links WITH extra per-link data (image, color, badge, mega-menu content)?**\n→ Use `COMPONENT_LIST` with a child component that has one `LINK` prop + the extra fields.\n\n**Is it a list of products/categories/blogs (not generic links)?**\n→ Use the specific prop type (`PRODUCT_LIST`, `CATEGORY_LIST`, `BLOG_LIST`), NOT `LIST_OF_LINK`.\n\n## Examples\n\n### Footer bottom bar: \"Privacy | Terms | Contact\"\nSimple flat list → `LIST_OF_LINK`:\n```json\n{ \"name\": \"bottomLinks\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.bottomLinks.links.map(link => <a href={link.href}>{link.label}</a>)`\n\n### Main nav menu with dropdowns: \"Women (→ Tops, Bottoms, Shoes)\"\nHierarchical but just labels + links → `LIST_OF_LINK` (sub-links built-in):\n```json\n{ \"name\": \"menu\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.menu.links.map(link => (<div>{link.label}{link.subLinks?.map(...)}</div>))`\n\n### Mega menu with images per column\nLinks PLUS image + custom layout per menu item → `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"menuItems\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-mega-menu-item\"] }\n// Child MegaMenuItem:\n{ \"props\": [\n { \"name\": \"mainLink\", \"type\": \"LINK\" },\n { \"name\": \"columnImage\", \"type\": \"IMAGE\" },\n { \"name\": \"subLinks\", \"type\": \"LIST_OF_LINK\" }\n]}\n```\n\n### Social icons: \"Instagram | Facebook | Twitter\" each with its own icon image\nList of links with an image each → `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"socials\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-social-link\"] }\n// Child SocialLink:\n{ \"props\": [\n { \"name\": \"link\", \"type\": \"LINK\" },\n { \"name\": \"icon\", \"type\": \"IMAGE\" }\n]}\n```\n\n## Old CUSTOM → Which Link Prop?\n\nWhen migrating an old CUSTOM/DYNAMIC_LIST:\n\n| Old customData shape | New prop |\n|----------------------|----------|\n| `OBJECT { link: LINK }` | `LINK` (flatten) |\n| `DYNAMIC_LIST of OBJECT { link: LINK }` | `LIST_OF_LINK` (if just label+href) or `COMPONENT_LIST` (if more fields) |\n| `DYNAMIC_LIST of OBJECT { link, image }` | `COMPONENT_LIST` + child component |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: LIST_OF_LINK }` | `LIST_OF_LINK` (subLinks are native) |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: DYNAMIC_LIST of OBJECT { links, images, cols } }` | `COMPONENT_LIST` + `MenuItem` child + nested `COMPONENT_LIST` for columns |\n\n## See Also\n- `get_migration_guide(\"component-composition-decision-guide\")` — when COMPONENT_LIST is overkill vs. repeated scalars vs. domain LIST types\n- `get_migration_guide(\"prop-runtime-shapes\")` — exact shapes for each link type\n- `get_migration_guide(\"component-renderer-limitations\")` — COMPONENT_LIST constraints\n- `get_framework_guide(\"navigation-patterns\")` — Router.navigate usage, link handling",
|
|
239
|
+
"content": "Several prop types can represent \"links\" in the new system. Choosing the right one depends on the shape of the data and what the store owner should control.\n\n## Decision Tree\n\n**Is it a single link?**\n→ Use `LINK`. Runtime: `IkasNavigationLink { href, label, subLinks, openInNewTab, ... }`\n\n**Is it a flat list of sibling links (no extra data per link)?**\n→ Use `LIST_OF_LINK`. Runtime: `IkasNavigationLinkList` with `.links: IkasNavigationLink[]`\n\n**Is it a hierarchical menu (links with sub-links, no extra data)?**\n→ Use `LIST_OF_LINK`. Each `IkasNavigationLink` already has `.subLinks: IkasNavigationLink[]`. The editor handles sub-link nesting natively.\n\n**Is it a list of links WITH extra per-link data (image, color, badge, mega-menu content)?**\n→ Use `COMPONENT_LIST` with a child component that has one `LINK` prop + the extra fields.\n\n**Is it a list of products/categories/blogs (not generic links)?**\n→ Use the specific prop type (`PRODUCT_LIST`, `CATEGORY_LIST`, `BLOG_LIST`), NOT `LIST_OF_LINK`.\n\n## Examples\n\n### Footer bottom bar: \"Privacy | Terms | Contact\"\nSimple flat list → `LIST_OF_LINK`:\n```json\n{ \"name\": \"bottomLinks\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.bottomLinks.links.map(link => <a href={link.href}>{link.label}</a>)`\n\n### Main nav menu with dropdowns: \"Women (→ Tops, Bottoms, Shoes)\"\nHierarchical but just labels + links → `LIST_OF_LINK` (sub-links built-in):\n```json\n{ \"name\": \"menu\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.menu.links.map(link => (<div>{link.label}{link.subLinks?.map(...)}</div>))`\n\n### Mega menu with images per column\nLinks PLUS image + custom layout per menu item → `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"menuItems\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-mega-menu-item\"] }\n// Child MegaMenuItem:\n{ \"props\": [\n { \"name\": \"mainLink\", \"type\": \"LINK\" },\n { \"name\": \"columnImage\", \"type\": \"IMAGE\" },\n { \"name\": \"subLinks\", \"type\": \"LIST_OF_LINK\" }\n]}\n```\n\n### Social icons: \"Instagram | Facebook | Twitter\" each with its own icon image\nList of links with an image each → `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"socials\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-social-link\"] }\n// Child SocialLink:\n{ \"props\": [\n { \"name\": \"link\", \"type\": \"LINK\" },\n { \"name\": \"icon\", \"type\": \"IMAGE\" }\n]}\n```\n\n## Old CUSTOM → Which Link Prop?\n\nWhen migrating an old CUSTOM/DYNAMIC_LIST:\n\n| Old customData shape | New prop |\n|----------------------|----------|\n| `OBJECT { link: LINK }` | `LINK` (flatten) |\n| `DYNAMIC_LIST of OBJECT { link: LINK }` | `LIST_OF_LINK` (if just label+href) or `COMPONENT_LIST` (if more fields) |\n| `DYNAMIC_LIST of OBJECT { link, image }` | `COMPONENT_LIST` + child component |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: LIST_OF_LINK }` | `LIST_OF_LINK` (subLinks are native) |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: DYNAMIC_LIST of OBJECT { links, images, cols } }` | `COMPONENT_LIST` + `MenuItem` child + nested `COMPONENT_LIST` for columns |\n\n## Authoring the default value (config shape)\n\nWhichever type you pick, the `defaultValue` in `ikas.config.json` is the typed object — NOT the runtime `.href` read shape, and NOT a JSON string:\n- `LINK` → `{ \"linkType\": \"EXTERNAL\", \"label\": \"Shop\", \"externalLink\": \"https://…\", \"subLinks\": [] }` (or `\"linkType\": \"PAGE\"` + `\"pageType\"`; `\"FILE\"` + `\"fileUrl\"`).\n- `LIST_OF_LINK` → `{ \"links\": [ { \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] } ] }`.\n\nThe legacy `{ label, href }` shape is rejected by the CLI. Full reference: `get_framework_guide(\"prop-types\")` → \"LINK and LIST_OF_LINK props\".\n\n## See Also\n- `get_migration_guide(\"component-composition-decision-guide\")` — when COMPONENT_LIST is overkill vs. repeated scalars vs. domain LIST types\n- `get_migration_guide(\"prop-runtime-shapes\")` — exact shapes for each link type\n- `get_migration_guide(\"component-renderer-limitations\")` — COMPONENT_LIST constraints\n- `get_framework_guide(\"navigation-patterns\")` — Router.navigate usage, link handling",
|
|
234
240
|
"tags": [
|
|
235
241
|
"migration",
|
|
236
242
|
"LINK",
|
|
@@ -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",
|
|
@@ -50,6 +50,25 @@
|
|
|
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
73
|
"<id-of-AccountInfoContent>",
|
|
55
74
|
"<id-of-AccountOrders>",
|
|
@@ -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
|
}
|