@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.
Files changed (94) hide show
  1. package/data/framework.json +191 -78
  2. package/data/migration.json +10 -4
  3. package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +13 -0
  4. package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +1 -1
  5. package/data/section-templates/account-info-section/ikas-config-snippet.json +19 -0
  6. package/data/section-templates/add-to-cart/ikas-config-snippet.json +17 -0
  7. package/data/section-templates/add-to-cart/index.tsx +16 -2
  8. package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +11 -3
  9. package/data/section-templates/add-to-cart/utils/cartLimits.ts +24 -0
  10. package/data/section-templates/blog-home-section/ikas-config-snippet.json +2 -0
  11. package/data/section-templates/blog-home-section/index.tsx +2 -1
  12. package/data/section-templates/blog-post-section/ikas-config-snippet.json +2 -0
  13. package/data/section-templates/blog-post-section/index.tsx +3 -2
  14. package/data/section-templates/bundle-products/hooks/useBundleProducts.ts +7 -6
  15. package/data/section-templates/bundle-products/ikas-config-snippet.json +20 -0
  16. package/data/section-templates/cart-section/components/EmptyState/index.tsx +1 -1
  17. package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +66 -24
  18. package/data/section-templates/cart-section/sub-components/CartItem/styles.css +14 -0
  19. package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +11 -3
  20. package/data/section-templates/cart-section/utils/cartLimits.ts +24 -0
  21. package/data/section-templates/cart-section/utils/cartLineLimits.ts +100 -0
  22. package/data/section-templates/category-images-section/children/CategoryImageItem/ikas-config-snippet.json +4 -0
  23. package/data/section-templates/category-images-section/ikas-config-snippet.json +15 -0
  24. package/data/section-templates/category-list-section/children/CardProductName/ikas-config-snippet.json +2 -1
  25. package/data/section-templates/category-list-section/ikas-config-snippet.json +21 -0
  26. package/data/section-templates/category-list-section/index.tsx +2 -1
  27. package/data/section-templates/component-renderer/additional/ProductDetail/index.tsx +5 -1
  28. package/data/section-templates/component-renderer/ikas-config-snippet.json +13 -0
  29. package/data/section-templates/favorites/ikas-config-snippet.json +18 -1
  30. package/data/section-templates/features-section/ikas-config-snippet.json +13 -0
  31. package/data/section-templates/footer-section/ikas-config-snippet.json +15 -0
  32. package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/index.tsx +1 -1
  33. package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +7 -0
  34. package/data/section-templates/header-section/children/CookieBar/ikas-config-snippet.json +3 -0
  35. package/data/section-templates/header-section/children/Navbar/components/CartSidebar/index.tsx +2 -1
  36. package/data/section-templates/header-section/children/Navbar/components/SearchModal/index.tsx +2 -1
  37. package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +27 -0
  38. package/data/section-templates/header-section/children/Navbar/index.tsx +6 -5
  39. package/data/section-templates/header-section/ikas-config-snippet.json +13 -0
  40. package/data/section-templates/header-section/sub-components/CartItem/index.tsx +66 -24
  41. package/data/section-templates/header-section/sub-components/CartItem/styles.css +14 -0
  42. package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +11 -3
  43. package/data/section-templates/header-section/utils/cartLimits.ts +24 -0
  44. package/data/section-templates/header-section/utils/cartLineLimits.ts +100 -0
  45. package/data/section-templates/hero-slider-section/children/HeroSliderItem/ikas-config-snippet.json +18 -0
  46. package/data/section-templates/hero-slider-section/children/HeroSliderItem/index.tsx +12 -7
  47. package/data/section-templates/hero-slider-section/ikas-config-snippet.json +13 -0
  48. package/data/section-templates/image-handling/ikas-config-snippet.json +53 -0
  49. package/data/section-templates/image-handling/index.tsx +5 -1
  50. package/data/section-templates/login-section/ikas-config-snippet.json +2 -0
  51. package/data/section-templates/navigation/components/CartSidebar/index.tsx +2 -1
  52. package/data/section-templates/navigation/components/SearchModal/index.tsx +2 -1
  53. package/data/section-templates/navigation/ikas-config-snippet.json +27 -0
  54. package/data/section-templates/navigation/index.tsx +6 -5
  55. package/data/section-templates/navigation/sub-components/CartItem/index.tsx +66 -24
  56. package/data/section-templates/navigation/sub-components/CartItem/styles.css +14 -0
  57. package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +11 -3
  58. package/data/section-templates/navigation/utils/cartLimits.ts +24 -0
  59. package/data/section-templates/navigation/utils/cartLineLimits.ts +100 -0
  60. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/ikas-config-snippet.json +17 -0
  61. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +16 -2
  62. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +92 -0
  63. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +175 -0
  64. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +141 -0
  65. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +47 -0
  66. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +25 -0
  67. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/ikas-config-snippet.json +18 -0
  68. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/ikas-config-snippet.json +20 -0
  69. package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +27 -0
  70. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +29 -0
  71. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/ikas-config-snippet.json +18 -1
  72. package/data/section-templates/product-detail-section/children/ProductDetailOffer/ikas-config-snippet.json +20 -0
  73. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/ikas-config-snippet.json +16 -0
  74. package/data/section-templates/product-detail-section/children/ProductDetailPrices/ikas-config-snippet.json +16 -0
  75. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/ikas-config-snippet.json +16 -0
  76. package/data/section-templates/product-detail-section/children/ProductDetailSku/ikas-config-snippet.json +16 -0
  77. package/data/section-templates/product-detail-section/children/ProductDetailVariant/ikas-config-snippet.json +17 -0
  78. package/data/section-templates/product-detail-section/hooks/useBundleProducts.ts +7 -6
  79. package/data/section-templates/product-detail-section/ikas-config-snippet.json +53 -0
  80. package/data/section-templates/product-detail-section/index.tsx +5 -1
  81. package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +120 -0
  82. package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +70 -0
  83. package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +11 -3
  84. package/data/section-templates/product-detail-section/utils/cartLimits.ts +24 -0
  85. package/data/section-templates/product-pricing/ikas-config-snippet.json +16 -0
  86. package/data/section-templates/product-slider-section/children/CardProductName/ikas-config-snippet.json +2 -1
  87. package/data/section-templates/product-slider-section/ikas-config-snippet.json +17 -0
  88. package/data/section-templates/register-section/ikas-config-snippet.json +2 -0
  89. package/data/section-templates/variant-selection/ikas-config-snippet.json +17 -0
  90. package/data/storefront-api.json +1046 -80
  91. package/data/storefront-types.json +9868 -6037
  92. package/dist/index.js +2578 -257
  93. package/dist/index.js.map +1 -1
  94. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  "project-structure": {
5
5
  "title": "Project Structure",
6
6
  "description": "How an ikas code components project is organized",
7
- "content": "An ikas code components project follows this structure:\n\n```\nmy-project/\n├── src/\n│ ├── global.css # Optional: project-wide unscoped CSS (variables, resets)\n│ └── components/\n│ ├── MyComponent/\n│ │ ├── index.tsx # Component implementation (Preact)\n│ │ ├── types.ts # TypeScript props interface\n│ │ └── styles.css # Scoped component styles\n│ └── index.ts # Component exports barrel\n├── ikas.config.json # Component definitions, props, metadata\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nEach component lives in its own directory under `src/components/`. The component's entry point is `index.tsx`, which exports a default Preact function component. The `types.ts` file defines the props interface matching the props declared in `ikas.config.json`. The `styles.css` file contains CSS that is automatically scoped to the component.\n\n### Project with Sub-Components\n\nWhen components grow complex, extract sub-components into `src/sub-components/`. See `get_framework_guide(\"sub-component-patterns\")` for full details.\n\n```\nmy-project/\n├── src/\n│ ├── components/\n│ │ ├── ProductList/\n│ │ │ ├── index.tsx # Imports from ../../sub-components/...\n│ │ │ ├── types.ts # Auto-generated (CLI-managed)\n│ │ │ └── styles.css # @import \"../../sub-components/.../styles.css\"\n│ │ └── index.ts\n│ └── sub-components/\n│ ├── ProductCard/\n│ │ ├── index.tsx # Sub-component with inline Props\n│ │ └── styles.css # Sub-component styles\n│ └── FilterSidebar/\n│ ├── index.tsx\n│ └── styles.css\n├── ikas.config.json\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\n**Key:** `src/components/` = registered components (in ikas.config.json). `src/sub-components/` = internal helpers (NOT in ikas.config.json).",
7
+ "content": "An ikas code components project follows this structure:\n\n```\nmy-project/\n├── src/\n│ ├── global.css # Optional: project-wide CSS, scoped to this project (variables, fonts, resets)\n│ └── components/\n│ ├── MyComponent/\n│ │ ├── index.tsx # Component implementation (Preact)\n│ │ ├── types.ts # TypeScript props interface\n│ │ └── styles.css # Scoped component styles\n│ └── index.ts # Component exports barrel\n├── ikas.config.json # Component definitions, props, metadata\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nEach component lives in its own directory under `src/components/`. The component's entry point is `index.tsx`, which exports a default Preact function component. The `types.ts` file defines the props interface matching the props declared in `ikas.config.json`. The `styles.css` file contains CSS that is automatically scoped to the component.\n\n### Project with Sub-Components\n\nWhen components grow complex, extract sub-components into `src/sub-components/`. See `get_framework_guide(\"sub-component-patterns\")` for full details.\n\n```\nmy-project/\n├── src/\n│ ├── components/\n│ │ ├── ProductList/\n│ │ │ ├── index.tsx # Imports from ../../sub-components/...\n│ │ │ ├── types.ts # Auto-generated (CLI-managed)\n│ │ │ └── styles.css # component styles (sub-component styles.css collected automatically)\n│ │ └── index.ts\n│ └── sub-components/\n│ ├── ProductCard/\n│ │ ├── index.tsx # Sub-component with inline Props\n│ │ └── styles.css # Sub-component styles\n│ └── FilterSidebar/\n│ ├── index.tsx\n│ └── styles.css\n├── ikas.config.json\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\n**Key:** `src/components/` = registered components (in ikas.config.json). `src/sub-components/` = internal helpers (NOT in ikas.config.json).",
8
8
  "tags": [
9
9
  "structure",
10
10
  "project",
@@ -17,7 +17,7 @@
17
17
  "ikas-config": {
18
18
  "title": "ikas.config.json Schema",
19
19
  "description": "Configuration file that defines components, their props, and metadata",
20
- "content": "The `ikas.config.json` file is the central configuration for your code components project.\n\n```json\n{\n \"name\": \"my-project\",\n \"version\": \"1.0.0\",\n \"globalStyles\": \"./src/global.css\",\n \"components\": [\n {\n \"id\": \"my-component\",\n \"name\": \"My Component\",\n \"entry\": \"./src/components/MyComponent/index.tsx\",\n \"styles\": \"./src/components/MyComponent/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": true,\n \"defaultValue\": \"Hello\",\n \"description\": \"The main heading text\"\n }\n ]\n },\n {\n \"id\": \"hero-banner\",\n \"name\": \"Hero Banner\",\n \"type\": \"section\",\n \"entry\": \"./src/components/HeroBanner/index.tsx\",\n \"styles\": \"./src/components/HeroBanner/styles.css\",\n \"props\": [\n {\n \"name\": \"heading\",\n \"displayName\": \"Heading\",\n \"type\": \"TEXT\",\n \"required\": true,\n \"defaultValue\": \"Welcome\"\n }\n ]\n }\n ]\n}\n```\n\n### Top-level fields:\n- `name` (string): Project name\n- `version` (string): Project version\n- `globalStyles` (string, optional): Path to a global CSS file (e.g., `\"./src/global.css\"`). See `get_framework_guide(\"global-css\")` for details\n- `components` (array): List of component definitions\n\n### Component fields:\n- `id` (string): Unique identifier (kebab-case)\n- `name` (string): Display name in the editor\n- `type` (string, optional): `\"component\"` (default) or `\"section\"` — sections are page-level containers (e.g. header, hero banner, footer), components are child elements placed inside sections (e.g. button, card, badge)\n- `entry` (string): Path to the component's entry file (index.tsx)\n- `styles` (string): Path to the component's CSS file\n- `props` (array): List of prop definitions\n- `isHeader` (boolean, optional): Mark this section as the store's header. Only for `type: \"section\"`. At most one component should have `isHeader: true`\n- `isFooter` (boolean, optional): Mark this section as the store's footer. Only for `type: \"section\"`. At most one component should have `isFooter: true`\n\n### Prop fields:\n- `name` (string): Prop name used in code (camelCase)\n- `displayName` (string): Label shown in the editor UI\n- `type` (string): One of the available prop types\n- `required` (boolean): Whether the prop must be set\n- `defaultValue` (any): Default value matching the prop type\n- `description` (string): Help text shown in editor\n- `groupId` (string, optional): Assign this prop to a prop group (references a group's `id`)\n\n### Prop Groups (optional):\nAdd `propGroups` array on a component to organize props into collapsible groups in the editor.\n- `id` (string): Unique group identifier within the component\n- `name` (string): Display name in editor\n- `description` (string, optional): Group description\n- `children` (array, optional): Nested groups (max 1 level)\n\nSee `get_framework_guide(\"prop-groups\")` for full details and examples.\n\n### Header & Footer Flags\nFor section-type components that serve as the store's header or footer, set `isHeader: true` or `isFooter: true`:\n```json\n{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"isHeader\": true,\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [...]\n}\n```\nThese flags tell the editor to treat the wrapper section as a common header/footer, which affects page composition (header/footer appear on all pages automatically).",
20
+ "content": "The `ikas.config.json` file is the central configuration for your code components project.\n\n```json\n{\n \"name\": \"my-project\",\n \"version\": \"1.0.0\",\n \"globalStyles\": \"./src/global.css\",\n \"components\": [\n {\n \"id\": \"my-component\",\n \"name\": \"My Component\",\n \"entry\": \"./src/components/MyComponent/index.tsx\",\n \"styles\": \"./src/components/MyComponent/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": true,\n \"defaultValue\": \"Hello\",\n \"description\": \"The main heading text\"\n }\n ]\n },\n {\n \"id\": \"hero-banner\",\n \"name\": \"Hero Banner\",\n \"type\": \"section\",\n \"entry\": \"./src/components/HeroBanner/index.tsx\",\n \"styles\": \"./src/components/HeroBanner/styles.css\",\n \"props\": [\n {\n \"name\": \"heading\",\n \"displayName\": \"Heading\",\n \"type\": \"TEXT\",\n \"required\": true,\n \"defaultValue\": \"Welcome\"\n }\n ]\n }\n ]\n}\n```\n\n### Top-level fields:\n- `name` (string): Project name\n- `version` (string): Project version\n- `globalStyles` (string, optional): Path to a global CSS file (e.g., `\"./src/global.css\"`). See `get_framework_guide(\"global-css\")` for details\n- `components` (array): List of component definitions\n\n### Component fields:\n- `id` (string): Unique identifier (kebab-case)\n- `name` (string): Display name in the editor\n- `type` (string, optional): `\"component\"` (default) or `\"section\"` — sections are page-level containers (e.g. header, hero banner, footer), components are child elements placed inside sections (e.g. button, card, badge)\n- `entry` (string): Path to the component's entry file (index.tsx)\n- `styles` (string): Path to the component's CSS file\n- `props` (array): List of prop definitions\n- `isHeader` (boolean, optional): Mark this section as the store's header. Only for `type: \"section\"`. At most one component should have `isHeader: true`\n- `isFooter` (boolean, optional): Mark this section as the store's footer. Only for `type: \"section\"`. At most one component should have `isFooter: true`\n\n### Prop fields:\n- `name` (string): Prop name used in code (camelCase)\n- `displayName` (string): Label shown in the editor UI\n- `type` (string): One of the available prop types\n- `required` (boolean): Whether the prop must be set\n- `defaultValue` (any): Default value matching the prop type. NOT allowed for dynamic merchant-data types (IMAGE, IMAGE_LIST, VIDEO, PRODUCT, PRODUCT_LIST, PRODUCT_ATTRIBUTE, PRODUCT_ATTRIBUTE_LIST, BRAND, BRAND_LIST, CATEGORY, CATEGORY_LIST, BLOG, BLOG_LIST, BLOG_CATEGORY, BLOG_CATEGORY_LIST, RAFFLE, RAFFLE_LIST) — they resolve from store data at render time, so the CLI rejects a default on these and the editor strips it\n- `description` (string): Help text shown in editor\n- `groupId` (string, optional): Assign this prop to a prop group (references a group's `id`)\n- `requiredProp` (string, optional): Show this prop in the editor panel only while the named BOOLEAN/ENUM prop on the same component satisfies the condition. A prop NAME, not an id. See `get_framework_guide(\"conditional-visibility\")`\n- `requiredPropValue` (boolean, optional): For a BOOLEAN `requiredProp` — which state shows this prop (defaults to `true`)\n- `requiredEnumOptionValue` (string, optional): For an ENUM `requiredProp` — the option value that shows this prop\n\n### Prop Groups (optional):\nAdd `propGroups` array on a component to organize props into collapsible groups in the editor.\n- `id` (string): Unique group identifier within the component\n- `name` (string): Display name in editor\n- `description` (string, optional): Group description\n- `children` (array, optional): Nested groups (max 1 level)\n- `requiredProp` / `requiredPropValue` / `requiredEnumOptionValue` (optional): Gate the whole group — it and everything under it are shown only while the named BOOLEAN/ENUM prop satisfies the condition. See `get_framework_guide(\"conditional-visibility\")`\n\nSee `get_framework_guide(\"prop-groups\")` for full details and examples.\n\n### Translations (optional)\nTechnical texts are shown in the editor's own language. The base fields (`name`, `displayName`, `description`) are the default texts; `translations` holds another editor language's texts keyed by locale (`en`). A store language's default prop values go under the component's `translations.<locale>.propValues` (by prop name) — they seed the section's values in that language.\n```json\n{\n \"id\": \"hero-banner\",\n \"name\": \"Kapak Bölümü\",\n \"translations\": {\n \"en\": { \"name\": \"Hero Banner\", \"propValues\": { \"heading\": \"Welcome\" } },\n \"de\": { \"propValues\": { \"heading\": \"Willkommen\" } }\n },\n \"props\": [\n {\n \"name\": \"heading\", \"displayName\": \"Başlık\", \"type\": \"TEXT\", \"defaultValue\": \"Hoş geldiniz\",\n \"translations\": { \"en\": { \"displayName\": \"Heading\", \"description\": \"The section heading\" } }\n }\n ],\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"İçerik\", \"translations\": { \"en\": { \"name\": \"Content\" } } }\n ]\n}\n```\nThe config gives the default texts. A text edited on the editor's \"Editor Texts\" page stays over the config (a reset button puts the config's text back); while `ikas theme dev` is running, the edited texts and a component's edited default and language values are written to the config on their own (on connecting and on every save). The editor's value wins over a hand edit of the same field in the config; a field not edited in the editor follows the config. To translate from an agent, use the editor's translations page tools (`list_translations`, `update_translations`, `manage_translation_languages`) rather than editing `translations` in the config by hand: they go through the editor and on to the config.\n\n### UI Texts (i18n)\nThe project's fixed UI texts — button labels, status lines, empty states — that the merchant does not edit per section but that must follow the store's language. Use a **prop** for content the merchant chooses per section (a heading, a banner text); use a **UI text** for a fixed text of a component's UI (`Sepete ekle`, `{{count}} adet kaldı`). Never hard-code such a text in JSX.\n\nThey are project-wide (shared by every component) and live in `src/i18n/`: `default.json` holds the base texts, `<locale>.json` a language's (`en.json`, `de.json`). Keys are dotted groups:\n```json\n{ \"common\": { \"addToCart\": \"Sepete ekle\" }, \"product\": { \"stockLeft\": \"{{count}} adet kaldı\" } }\n```\n- Add or change one with `ikas-component config set-text --key common.addToCart --text \"Sepete ekle\"` (`--locale en` for a language's text); `config remove-text --key <key>` removes it (from every language, or with `--locale` only that language's); `config list-texts` lists the texts and the keys each language lacks; `config rename-text --key product --to item` renames a text or a whole group in every language (then change the `t()` calls: the type check reports the old keys). Plural forms: give each form its own key with its plural suffix (`cart.itemCount_one`: \"{{count}} item\", `cart.itemCount_other`: \"{{count}} items\"; an optional `_zero` for 0) and call `t(\"cart.itemCount\", { count })` — it picks the form the store language's rules name (`Intl.PluralRules`: French counts 0 as `one`), else `_other`; never pick the form in the component (`count === 1 ? …` is wrong in many languages). A language with more forms (`_few`, `_many`: Russian, Polish, Arabic) gets them in its own file alone, with the `_other` text's `{{params}}` (`config set-text --locale ru --key cart.itemCount_few`; the translations page lists them for that language), else it shows `_other`. The first `set-text` adds `\"i18n\": \"./src/i18n\"` to `ikas.config.json`. A language's text must use exactly the base text's `{{params}}`; a base text cannot be empty; a language file is named by a real language code.\n- Change the texts only with these commands (or on the editor's page), never by editing the JSON files by hand: a text that breaks the rules is left out of the editor and the store, and `build`/`publish` stop until it is fixed — after a change, `config list-texts` must report no `errors`. A base text's `{{params}}` are what the code's `t()` calls pass: the editor never changes them; with `set-text` change them together with the `t()` calls, after removing or rewriting the languages' texts that use the old ones.\n- Import `t` from the storefront package: `import { t } from \"@ikas/bp-storefront\"`, then `t(\"common.addToCart\")`, `t(\"product.stockLeft\", { count: 3 })`. The build binds it to the project's own texts (each project on a store reads its own); it reads the store language's text and falls back to the base text. The CLI keeps `src/i18n/index.d.ts` (types only; do not edit it) in step with `default.json`, so a missing key or param is a type error. The store's common texts (the ikas set: labels, placeholders, validation messages, ...) are read with the same `t` under the `ikas:` prefix: `t(\"ikas:validation.required\")`, `t(\"ikas:validation.min\", { min: 3 })`; their keys and params are typed too, and a project needs no texts of its own for them. Never read `t` through `import * as` or re-export it: the build refuses both.\n- On the editor's \"Store Translations\" page the texts are listed under \"UI Texts\", one item per top-level group; while `ikas theme dev` runs, edits there are written to the `src/i18n` files (the editor's value wins). An agent translates them with `list_translations` / `update_translations` (`area: \"i18n\"`, item `<projectId>:<group>`, key = the dotted path, locale `\"default\"` for the base texts). The texts travel with the published components and with a design asset.\n\n### Header & Footer Flags\nFor section-type components that serve as the store's header or footer, set `isHeader: true` or `isFooter: true`:\n```json\n{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"isHeader\": true,\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [...]\n}\n```\nThese flags tell the editor to treat the wrapper section as a common header/footer, which affects page composition (header/footer appear on all pages automatically).",
21
21
  "tags": [
22
22
  "config",
23
23
  "configuration",
@@ -29,13 +29,16 @@
29
29
  "groupId",
30
30
  "isHeader",
31
31
  "isFooter",
32
- "globalStyles"
32
+ "globalStyles",
33
+ "requiredProp",
34
+ "conditional",
35
+ "visibility"
33
36
  ]
34
37
  },
35
38
  "prop-types": {
36
39
  "title": "Available Prop Types",
37
40
  "description": "All prop types that can be used in ikas.config.json",
38
- "content": "Props define what the store editor can configure for each component. Each prop has a `type` that determines the editor UI and the TypeScript type received in your component.\n\n| Type | Editor UI | TypeScript Type | Description |\n|------|-----------|----------------|-------------|\n| `TEXT` | Text input | `string` | Single-line text |\n| `RICH_TEXT` | Rich text editor | `string` | HTML rich text content |\n| `NUMBER` | Number input | `number` | Numeric value |\n| `NUMBER_RANGE` | Number range input | `IkasNumberRange` | Number range with min, max, interval, and unit |\n| `BOOLEAN` | Toggle switch | `boolean` | True/false toggle |\n| `IMAGE` | Image picker | `IkasImage | null` | Image from editor. Use `getDefaultSrc(image)` for URL |\n| `IMAGE_LIST` | Image list picker | `IkasImageList` | Multiple images from editor |\n| `VIDEO` | Video picker | `IkasVideo | null` | Video from editor |\n| `SVG` | SVG upload | `string` | Raw inline SVG markup (the `<svg>…</svg>` source). Render with `dangerouslySetInnerHTML` so it stays a styleable vector. NOT served through the image CDN — this is the way to ship logos/icons as vectors. |\n| `SVG_LIST` | SVG list upload | `string[]` | List of raw inline SVG markup strings. Render each with `dangerouslySetInnerHTML`. |\n| `DATE` | Date picker | `Date | string` | Date value |\n| `LINK` | Link editor | `IkasNavigationLink | null` | Navigation link with href, label, subLinks |\n| `LIST_OF_LINK` | Link list editor | `IkasNavigationLinkList` | List of navigation links |\n| `COLOR` | Color picker | `string` | CSS color value (hex, rgb, etc.) |\n| `PRODUCT` | Product picker | `IkasProduct | null` | Single product reference |\n| `PRODUCT_LIST` | Product list picker | `IkasProductList` | Product list with `.data` (products array), `.filters`, `.sort`, `.page`, etc. |\n| `PRODUCT_ATTRIBUTE` | Product attribute picker | `IkasProductAttributeValue | null` | Single product attribute value |\n| `PRODUCT_ATTRIBUTE_LIST` | Product attribute list picker | `IkasProductAttributeValue[]` | Multiple product attribute values |\n| `CATEGORY` | Category picker | `IkasCategory | null` | Single category reference |\n| `CATEGORY_LIST` | Category list picker | `IkasCategoryList` | Multiple category references |\n| `BRAND` | Brand picker | `IkasBrand | null` | Single brand reference |\n| `BRAND_LIST` | Brand list picker | `IkasBrandList` | Multiple brand references |\n| `BLOG` | Blog post picker | `IkasBlog | null` | Single blog post reference |\n| `BLOG_LIST` | Blog post list picker | `IkasBlogList` | Multiple blog post references |\n| `BLOG_CATEGORY` | Blog category picker | `IkasBlogCategory | null` | Single blog category reference |\n| `BLOG_CATEGORY_LIST` | Blog category list picker | `IkasBlogCategoryList` | Multiple blog category references |\n| `TYPE` | Type selector | Depends on typeId | Structured type (padding, margin, size, etc.). Available for both components and sections (sections have a restricted whitelist). |\n| `ENUM` | Dropdown selector | `string` | Enum-based style type (flex-direction, justify-content, align-items, etc.). Uses `enumTypeId`. Available for both components and sections. |\n| `COMPONENT` | Component slot | `any` | A single child component slot. Store owners can place a component in this slot from the editor. Render with `<IkasComponentRenderer>`. Before using, see `get_migration_guide(\"component-composition-decision-guide\")`. |\n| `COMPONENT_LIST` | Component list slot | `any` | A list of child components. Store owners can add multiple components from the editor. Render with `<IkasComponentRenderer>`. Before using, see `get_migration_guide(\"component-composition-decision-guide\")`. |\n\n### COMPONENT & COMPONENT_LIST props (child component slots)\n\nThese prop types enable **slot-based** architectures where store owners can drag child components into your section/component from the editor.\n\n- `COMPONENT` — a single child component slot\n- `COMPONENT_LIST` — a list of child components\n\n**Rendering:** Use the `<IkasComponentRenderer>` wrapper from `@ikas/bp-storefront`:\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function MySection({ title, cardList, ...props }: Props) {\n return (\n <section>\n <h2>{title}</h2>\n <div className=\"cards\">\n {/* COMPONENT_LIST — render a list of child components */}\n <IkasComponentRenderer\n id=\"card-list\"\n components={cardList as any[]}\n parentProps={props}\n />\n </div>\n </section>\n );\n}\nexport default MySection;\n```\n\n**Key rules:**\n- Always pass `parentProps={props}` so child components can access parent data via dynamic values\n- Cast the prop value: `components={myList as any[]}` for COMPONENT_LIST, `components={[myComp] as any[]}` for COMPONENT\n- `<IkasComponentRenderer>` handles rendering, styling, and reactivity of child components automatically\n\n**Config example (COMPONENT_LIST):**\n```json\n{\n \"name\": \"cardList\",\n \"displayName\": \"Card List\",\n \"type\": \"COMPONENT_LIST\"\n}\n```\n\n**Config example (COMPONENT):**\n```json\n{\n \"name\": \"headerSlot\",\n \"displayName\": \"Header Slot\",\n \"type\": \"COMPONENT\"\n}\n```\n\n### Restricting which components can appear (`filteredComponentIds`)\n\nA COMPONENT or COMPONENT_LIST prop may include a `filteredComponentIds: string[]` field to limit which sibling components can be placed inside it. Each entry must be the **opaque random id** of a component already defined in `ikas.config.json` — e.g. `\"7ojrigep-Eml9n5sN3i\"`.\n\n**Ids are not derivable from component names. Do NOT compose them as `${projectId}-${name}`.** The CLI will reject any unknown id with a structured error listing the valid `{id, name}` pairs.\n\n**Recommended workflow:**\n\n1. Create the child component(s) first. The CLI prints the new id in its JSON response:\n ```bash\n npx ikas-component config add-component --name Navbar --type component\n # → { \"success\": true, \"componentId\": \"7ojrigep-Eml9n5sN3i\", ... }\n ```\n2. Capture the `componentId` and reuse it in the parent's `filteredComponentIds`:\n ```bash\n npx ikas-component config add-component --name Header --type section \\\n --props '[{\"name\":\"components\",\"type\":\"COMPONENT_LIST\",\"filteredComponentIds\":[\"7ojrigep-Eml9n5sN3i\"]}]'\n ```\n\nOr create the parent first without `filteredComponentIds`, then attach it once children exist:\n```bash\nnpx ikas-component config add-prop Header components --type COMPONENT_LIST \\\n --filteredComponentIds '[\"7ojrigep-Eml9n5sN3i\"]'\n```\n\n**Discovering existing ids:** `npx ikas-component config list` returns a JSON document with every component's `{ id, name, type, props… }`. Use that when joining a project mid-flight.\n\n### IMAGE type example:\n```json\n{\n \"name\": \"heroImage\",\n \"displayName\": \"Hero Image\",\n \"type\": \"IMAGE\",\n \"required\": false\n}\n```\nAccess in component: `getDefaultSrc(props.heroImage)` (import `getDefaultSrc` from `@ikas/bp-storefront`)\n\n### SVG and SVG_LIST props (inline vector graphics)\n\nUse `SVG` / `SVG_LIST` when you need a **vector** asset (logo, icon, decorative shape) that must scale crisply and be styleable with CSS — things the raster image CDN cannot do. The editor uploads the file and stores the **raw `<svg>…</svg>` markup as a string**; it is NOT uploaded to the image CDN, so there is no URL and no `IkasImage` wrapper. Uploads are capped at 64 KB and SVGs containing embedded base64 raster images are rejected by the editor.\n\n- `SVG` → `string` (one SVG's markup)\n- `SVG_LIST` → `string[]` (multiple SVGs; the editor allows selecting several files at once)\n\n**Rendering:** inject the markup directly. Because it is the same `<svg>` source the user uploaded, you can target it with CSS (e.g. `.icon svg { width: 24px; fill: currentColor; }`).\n```tsx\nimport { Props } from \"./types\";\n\nexport function Logo({ logo, partnerLogos }: Props) {\n return (\n <div className=\"logos\">\n {logo && <span className=\"icon\" dangerouslySetInnerHTML={{ __html: logo }} />}\n {(partnerLogos ?? []).map((svg, i) => (\n <span key={i} className=\"icon\" dangerouslySetInnerHTML={{ __html: svg }} />\n ))}\n </div>\n );\n}\nexport default Logo;\n```\n\n**Config example (SVG):**\n```json\n{\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"SVG\"\n}\n```\n\n**Config example (SVG_LIST):**\n```json\n{\n \"name\": \"partnerLogos\",\n \"displayName\": \"Partner Logos\",\n \"type\": \"SVG_LIST\"\n}\n```\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name logo --displayName Logo --type SVG\n```\n\n**When NOT to use:** for photos/raster artwork or anything that should be optimized/resized by the CDN, use `IMAGE` / `IMAGE_LIST` instead. SVG is only for hand-authored vector markup.\n\n### Prop grouping\nProps can be assigned to groups via `groupId` for organized editor sidebar display. See `get_framework_guide(\"prop-groups\")` for details.\n\n### Style Props: TYPE and ENUM\n\nThere are two prop types for style values:\n\n- **TYPE** — Structured types with numeric values and units (padding, margin, border-radius, sizes, etc.). Uses `typeId`.\n- **ENUM** — Enum types rendered as dropdown selectors (flex-direction, justify-content, align-items, etc.). Uses `enumTypeId`.\n\nBoth are available for components and sections. For sections, TYPE props are limited to a whitelist of style types. Use `list-types --component-type section` to see section-allowed types.\n\n### TYPE prop (structured types)\nThe `TYPE` prop lets you use structured storefront types like PaddingStyleType, MarginStyleType, SizeStyleType, etc. Available for both components and sections (sections have a restricted whitelist of style types).\n\n**Workflow:**\n1. Run `npx ikas-component config list-types` to get available types (requires dev server running with editor connected)\n2. Use the `typeId` from the output when adding the prop\n3. For sections: `npx ikas-component config list-types --component-type section` to see only section-allowed types\n\n**Example config:**\n```json\n{\n \"name\": \"spacing\",\n \"displayName\": \"Spacing\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-PaddingStyleType\"\n}\n```\n\n**Array example** (append `_array` to typeId):\n```json\n{\n \"name\": \"margins\",\n \"displayName\": \"Margins\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginStyleType_array\"\n}\n```\nThis generates `margins?: MarginStyleType[]` in types.ts.\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name spacing --displayName Spacing --type TYPE --typeId \"@ikas/bp-storefront-models-PaddingStyleType\"\n```\n\n### ENUM prop (enum style types)\nThe `ENUM` prop lets you use enum-based types that render as dropdown selectors. There are two kinds of enum types:\n\n1. **Built-in enums** (prefix `@ikas/`): FlexDirectionStyleType, JustifyContentStyleType, AlignItemsStyleType, etc. Available from `list-types` when editor is connected.\n2. **Custom enums**: Created with `config add-enum`. Works offline, no editor needed.\n\n**IMPORTANT:** `add-prop --type ENUM` validates that the `enumTypeId` references an existing enum. You MUST create custom enums first with `config add-enum` before using them. The command will reject unknown enum IDs.\n\n**Custom enum workflow (recommended for AI-driven generation):**\n```bash\n# Step 1: Create the enum FIRST\nnpx ikas-component config add-enum --name \"AspectRatio\" --options '{\"Square\":\"1/1\",\"Landscape\":\"16/9\",\"Portrait\":\"3/4\"}'\n# Returns: {\"success\":true,\"enumId\":\"aBcDeFgHiJ\",...}\n\n# Step 2: Use the returned enumId in add-prop\nnpx ikas-component config add-prop --component MyComp --name aspectRatio --displayName \"Aspect Ratio\" --type ENUM --enumTypeId aBcDeFgHiJ\n```\n\n**Built-in enum workflow:**\n1. Run `npx ikas-component config list-types` — enum types have `category: \"enum\"` in the output\n2. Use the `enumTypeId` from the output when adding the prop\n\n**Example config (built-in enum):**\n```json\n{\n \"name\": \"direction\",\n \"displayName\": \"Direction\",\n \"type\": \"ENUM\",\n \"enumTypeId\": \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n}\n```\nThis generates `direction?: string` in types.ts.\n\n**CLI command (built-in enum):**\n```bash\nnpx ikas-component config add-prop --component MyComp --name direction --displayName Direction --type ENUM --enumTypeId \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n```\n\n**Note:** `list-types` requires dev server with editor connected. Custom enums (`config add-enum`) work offline.",
41
+ "content": "Props define what the store editor can configure for each component. Each prop has a `type` that determines the editor UI and the TypeScript type received in your component.\n\n| Type | Editor UI | TypeScript Type | Description |\n|------|-----------|----------------|-------------|\n| `TEXT` | Text input | `string` | Single-line text |\n| `RICH_TEXT` | Rich text editor | `string` | HTML rich text content |\n| `NUMBER` | Number input | `number` | Numeric value. Authored `defaultValue` must be a finite number — an empty string, a non-numeric string (e.g. `12px`), NaN or Infinity are rejected by the CLI. |\n| `NUMBER_RANGE` | Number range input | `IkasNumberRange` | Number range with min, max, interval, and unit |\n| `BOOLEAN` | Toggle switch | `boolean` | True/false toggle |\n| `IMAGE` | Image picker | `IkasImage | null` | Image from editor. Use `getDefaultSrc(image)` for URL |\n| `IMAGE_LIST` | Image list picker | `IkasImageList` | Multiple images from editor |\n| `VIDEO` | Video picker | `IkasVideo | null` | Video from editor |\n| `SVG` | SVG upload | `string` | Raw inline SVG markup (the `<svg>…</svg>` source). Normalize with `normalizeSvg(value, { idPrefix })` before rendering via `dangerouslySetInnerHTML`. NOT served through the image CDN — this is the way to ship logos/icons as vectors. |\n| `SVG_LIST` | SVG list upload | `string[]` | List of raw inline SVG markup strings. Normalize each with `normalizeSvg(svg, { idPrefix })` (unique per item) before rendering with `dangerouslySetInnerHTML`. |\n| `DATE` | Date picker | `Date | string` | Date value |\n| `LINK` | Link editor | `IkasNavigationLink | null` | Navigation link. Read at runtime as `.href`/`.label`/`.subLinks`; authored `defaultValue` is a typed object (`linkType`+`externalLink`/`pageType`+`subLinks`). See the LINK section below. |\n| `LIST_OF_LINK` | Link list editor | `IkasNavigationLinkList` | List of navigation links. Authored `defaultValue` is `{ \"links\": [ <link>, … ] }`. See the LINK section below. |\n| `COLOR` | Color picker | `string` | CSS color value (hex, rgb, etc.) |\n| `PRODUCT` | Product picker | `IkasProduct | null` | Single product reference |\n| `PRODUCT_LIST` | Product list picker | `IkasProductList` | Product list with `.data` (products array), `.filters`, `.sort`, `.page`, etc. |\n| `PRODUCT_ATTRIBUTE` | Product attribute picker | `IkasProductAttributeDetail | null` | Wrapper `{ product, attributePropValue }` — NOT the value itself; read it with `getAttributeDetailValues(prop)` |\n| `PRODUCT_ATTRIBUTE_LIST` | Product attribute list picker | `IkasProductAttributeList` | Wrapper `{ product, attributeListPropValue }` — a single object, NOT an array; read it with `getAttributeListValues(prop)` |\n| `CATEGORY` | Category picker | `IkasCategory | null` | Single category reference |\n| `CATEGORY_LIST` | Category list picker | `IkasCategoryList` | Multiple category references |\n| `BRAND` | Brand picker | `IkasBrand | null` | Single brand reference |\n| `BRAND_LIST` | Brand list picker | `IkasBrandList` | Multiple brand references |\n| `BLOG` | Blog post picker | `IkasBlog | null` | Single blog post reference |\n| `BLOG_LIST` | Blog post list picker | `IkasBlogList` | Multiple blog post references |\n| `BLOG_CATEGORY` | Blog category picker | `IkasBlogCategory | null` | Single blog category reference |\n| `BLOG_CATEGORY_LIST` | Blog category list picker | `IkasBlogCategoryList` | Multiple blog category references |\n| `TYPE` | Type selector | Depends on typeId | Structured type (padding, margin, size, etc.). Available for both components and sections (sections have a restricted whitelist). |\n| `ENUM` | Dropdown selector | `string` | Enum-based style type (flex-direction, justify-content, align-items, etc.). Uses `enumTypeId`. Available for both components and sections. |\n| `COMPONENT` | Component slot | `any` | A single child component slot. Store owners can place a component in this slot from the editor. Render with `<IkasComponentRenderer>`. Before using, see `get_migration_guide(\"component-composition-decision-guide\")`. |\n| `COMPONENT_LIST` | Component list slot | `any` | A list of child components. Store owners can add multiple components from the editor. Render with `<IkasComponentRenderer>`. Before using, see `get_migration_guide(\"component-composition-decision-guide\")`. |\n\n### Dynamic props have NO default value\n\nMerchant-data reference types resolve from the store's live data at render time, so they cannot carry a static `defaultValue`: IMAGE, IMAGE_LIST, VIDEO, PRODUCT, PRODUCT_LIST, PRODUCT_ATTRIBUTE, PRODUCT_ATTRIBUTE_LIST, BRAND, BRAND_LIST, CATEGORY, CATEGORY_LIST, BLOG, BLOG_LIST, BLOG_CATEGORY, BLOG_CATEGORY_LIST, RAFFLE, RAFFLE_LIST. The CLI (`add-prop`/`update-prop`) rejects a `defaultValue` on these with an error, and the editor strips any that slips through a hand-edited config. Omit `defaultValue` entirely for these props. (Scalar, LINK, COMPONENT, TYPE and SVG props DO support defaults.)\n\n### COMPONENT & COMPONENT_LIST props (child component slots)\n\nThese prop types enable **slot-based** architectures where store owners can drag child components into your section/component from the editor.\n\n- `COMPONENT` — a single child component slot\n- `COMPONENT_LIST` — a list of child components\n\n**Rendering:** Use the `<IkasComponentRenderer>` wrapper from `@ikas/bp-storefront`:\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function MySection({ title, cardList, ...props }: Props) {\n return (\n <section>\n <h2>{title}</h2>\n <div className=\"cards\">\n {/* COMPONENT_LIST — render a list of child components */}\n <IkasComponentRenderer\n id=\"card-list\"\n components={cardList as any[]}\n parentProps={props}\n />\n </div>\n </section>\n );\n}\nexport default MySection;\n```\n\n**Key rules:**\n- Always pass `parentProps={props}` so child components can access parent data via dynamic values\n- Cast the prop value: `components={myList as any[]}` for COMPONENT_LIST, `components={[myComp] as any[]}` for COMPONENT\n- `<IkasComponentRenderer>` handles rendering, styling, and reactivity of child components automatically\n\n**Config example (COMPONENT_LIST):**\n```json\n{\n \"name\": \"cardList\",\n \"displayName\": \"Card List\",\n \"type\": \"COMPONENT_LIST\"\n}\n```\n\n**Config example (COMPONENT):**\n```json\n{\n \"name\": \"headerSlot\",\n \"displayName\": \"Header Slot\",\n \"type\": \"COMPONENT\"\n}\n```\n\n### Restricting which components can appear (`filteredComponentIds`)\n\nA COMPONENT or COMPONENT_LIST prop may include a `filteredComponentIds: string[]` field to limit which sibling components can be placed inside it. Each entry must be the **opaque random id** of a component already defined in `ikas.config.json` — e.g. `\"7ojrigep-Eml9n5sN3i\"`.\n\n**Ids are not derivable from component names. Do NOT compose them as `${projectId}-${name}`.** The CLI will reject any unknown id with a structured error listing the valid `{id, name}` pairs.\n\n**Recommended workflow:**\n\n1. Create the child component(s) first. The CLI prints the new id in its JSON response:\n ```bash\n npx ikas-component config add-component --name Navbar --type component\n # → { \"success\": true, \"componentId\": \"7ojrigep-Eml9n5sN3i\", ... }\n ```\n2. Capture the `componentId` and reuse it in the parent's `filteredComponentIds`:\n ```bash\n npx ikas-component config add-component --name Header --type section \\\n --props '[{\"name\":\"components\",\"type\":\"COMPONENT_LIST\",\"filteredComponentIds\":[\"7ojrigep-Eml9n5sN3i\"]}]'\n ```\n\nOr create the parent first without `filteredComponentIds`, then attach it once children exist:\n```bash\nnpx ikas-component config add-prop Header components --type COMPONENT_LIST \\\n --filteredComponentIds '[\"7ojrigep-Eml9n5sN3i\"]'\n```\n\n**Discovering existing ids:** `npx ikas-component config list` returns a JSON document with every component's `{ id, name, type, props… }`. Use that when joining a project mid-flight.\n\n### IMAGE type example:\n```json\n{\n \"name\": \"heroImage\",\n \"displayName\": \"Hero Image\",\n \"type\": \"IMAGE\",\n \"required\": false\n}\n```\nAccess in component: `getDefaultSrc(props.heroImage)` (import `getDefaultSrc` from `@ikas/bp-storefront`)\n\n### SVG and SVG_LIST props (inline vector graphics)\n\nUse `SVG` / `SVG_LIST` when you need a **vector** asset (logo, icon, decorative shape) that must scale crisply and be styleable with CSS — things the raster image CDN cannot do. The editor uploads the file and stores the **raw `<svg>…</svg>` markup as a string**; it is NOT uploaded to the image CDN, so there is no URL and no `IkasImage` wrapper. Uploads are sanitized by the editor at upload time — XSS vectors (`<script>`, `on*` handlers, `javascript:` hrefs, `<foreignObject>`) are stripped — in addition to the existing 64 KB cap and rejection of SVGs containing embedded base64 raster images.\n\n- `SVG` → `string` (one SVG's markup)\n- `SVG_LIST` → `string[]` (multiple SVGs; the editor allows selecting several files at once)\n\n**Rendering:** inject the markup directly. Because it is the same `<svg>` source the user uploaded, you can target it with CSS (e.g. `.icon svg { width: 24px; fill: currentColor; }`).\n\n**Normalize before injecting:** call `normalizeSvg(value, { idPrefix })` from `@ikas/bp-storefront` before setting `dangerouslySetInnerHTML`, so internal ids/`clipPath`/`url(#…)` references get scoped per instance and don't collide when the same SVG (or multiple `SVG_LIST` items) render more than once on a page. For `SVG_LIST`, give each item a deterministic per-item `idPrefix`, e.g. `` `${propKey}-${i}` ``. By default `normalizeSvg` also strips fixed `width`/`height` and ensures a `viewBox` (so CSS controls size); pass `{ color: \"currentColor\" }` to recolor single-color icons via CSS `color` (omit it for multi-color SVGs). This is opt-in for code-components — the theme's built-in SVG element normalizes automatically, but your own `dangerouslySetInnerHTML` injections must call it explicitly.\n```tsx\nimport { normalizeSvg } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function Logo({ logo, partnerLogos }: Props) {\n return (\n <div className=\"logos\">\n {logo && (\n <span className=\"icon\" dangerouslySetInnerHTML={{ __html: normalizeSvg(logo, { idPrefix: \"logo\" }) }} />\n )}\n {(partnerLogos ?? []).map((svg, i) => (\n <span key={i} className=\"icon\" dangerouslySetInnerHTML={{ __html: normalizeSvg(svg, { idPrefix: `partner-logo-${i}` }) }} />\n ))}\n </div>\n );\n}\nexport default Logo;\n```\n\n**Config example (SVG):**\n```json\n{\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"SVG\"\n}\n```\n\n**Config example (SVG_LIST):**\n```json\n{\n \"name\": \"partnerLogos\",\n \"displayName\": \"Partner Logos\",\n \"type\": \"SVG_LIST\"\n}\n```\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name logo --displayName Logo --type SVG\n```\n\n**When NOT to use:** for photos/raster artwork or anything that should be optimized/resized by the CDN, use `IMAGE` / `IMAGE_LIST` instead. SVG is only for hand-authored vector markup.\n\n**Providing a default value:** A default value is a raw <svg>…</svg> string (SVG) or an array of them (SVG_LIST). It must be a single well-formed <svg> root, ≤ 64 KB, with no embedded base64 raster images; it is sanitized on load. Invalid defaults are rejected by the CLI (add-prop / add-component hard-fail with the reason) and silently dropped by the editor when the component is placed, so verify your default is valid. It must also RENDER VISIBLY: an open path with no enclosed area (e.g. the single horizontal line M4 12h16) is invisible under the default fill:black; stroke:none. Give the shape real area and set a color - prefer fill=currentColor (or an explicit stroke with stroke-width) so the icon inherits the theme text color. Example: \"defaultValue\": \"<svg viewBox=\\\"0 0 24 24\\\"><path fill=\\\"currentColor\\\" d=\\\"M12 2 2 22h20z\\\"/></svg>\".\n\n### SVG normalization & sanitization -- the theme-developer standard\n\n**Responsibility split (read this first):**\n- **Security/sanitization is the editor's job, not yours.** Every SVG is sanitized (DOMPurify) on upload, on programmatic writes, AND at render -- you never sanitize in component code.\n- **Normalization (size / color / id-scoping) is `normalizeSvg`'s job.** The theme's built-in SVG element applies it automatically; in your own code component you MUST call `normalizeSvg(value, { idPrefix })` before `dangerouslySetInnerHTML`. There is ONE shared util -- do not reinvent per theme: `import { normalizeSvg } from \"@ikas/bp-storefront\"`.\n\n`normalizeSvg(svg, options)` options: `{ idPrefix: string; scopeIds?: boolean = true; color?: \"currentColor\"; size?: boolean = true }`.\n\n**1. Size.** `size: true` (default) synthesizes a `viewBox` from numeric (unitless/px) `width`/`height` when missing, then strips `width`/`height` so CSS controls size. If there is no viewBox AND size cannot be synthesized (e.g. `%`/`em` dims), width/height are kept (so the icon is not left with zero intrinsic size). Merchants are NOT required to upload viewBox'd or size-less SVGs -- normalize handles it. Size icons with CSS: `.icon svg { width: 24px; height: 24px; }`.\n\n**2. Color.** Opt-in `{ color: \"currentColor\" }` rewrites `fill`/`stroke` (attributes AND `style:` declarations) to `currentColor`, but PRESERVES `none` and `url(...)` (gradients/patterns). So outline icons (`fill=\"none\" stroke=\"...\"`) keep `fill:none` and get `stroke:currentColor` -- no special-casing needed. For MULTI-COLOR SVGs, OMIT `color` to keep the original colors. Rule of thumb: single-color icons -> pass `currentColor`; multi-color -> omit.\n\n**3. Collision (id / clipPath / url(#...)).** Solved in code, not by the editor. `scopeIds: true` (default) prefixes every declared `id` and every `url(#...)` / `href=\"#...\"` reference with `idPrefix`, so the same SVG rendered multiple times (or multiple SVG_LIST items) cannot collide. Give each render an instance-unique prefix -- the built-in element uses its element id; for SVG_LIST use a per-item prefix like `` `${propKey}-${i}` ``.\n\n**4. Sanitization -- what the editor strips.** Beyond the 64 KB cap and base64-raster rejection, the editor strips `<script>`, `on*` event handlers (onload/onclick/onerror/...), `javascript:` hrefs, and `<foreignObject>`. This runs on file upload, on programmatic `update_section_prop` writes, and again at render -- so stored and rendered markup is always safe. `normalizeSvg` is NOT a sanitizer (pure string transforms); security lives in the separate sanitize/validate layer and is automatic.\n\n**5. One shared pattern.** Yes -- `normalizeSvg` from `@ikas/bp-storefront` is the single, isomorphic (SSR/browser/node) util. Do not hand-roll size/color/id-scoping per theme.\n\n**Naming.** The editor shows each SVG's `class` attribute as its display name in the list; SVG / SVG_LIST items with no class render as `svg-1`, `svg-2`, .... Set `class=\"my-icon\"` in the markup to name it.\n\n### NUMBER_RANGE prop (bounded numeric slider)\n\nA `NUMBER_RANGE` prop renders a **slider + number input** and gives the store owner one bounded number with an optional unit. Configure the slider with a **required** `numberRangeData` object on the prop (`min` and `max` are mandatory; `interval` and `unit` are optional). A NUMBER_RANGE prop authored without it is rejected:\n\n- `min` (integer) — lower bound\n- `max` (integer) — upper bound, must be greater than `min`\n- `interval` (positive integer, optional) — the step increment; typed values snap to `min + k·interval`, and a `0`/negative interval is dropped (step falls back to 1)\n- `unit` (string, optional) — suffix shown next to the number input (e.g. `\"px\"`, `\"%\"`)\n\n**Runtime read shape** is `IkasNumberRange`: read `props.myRange.value` (a number) and `props.myRange.unit` (string or null) — e.g. `props.gap.value + (props.gap.unit ?? \"px\")`.\n\n**Authored `defaultValue`** is a typed object `{ \"value\": <number>, \"unit\": \"px\" | null }` — NOT a bare number and NOT a JSON string. When the prop defines `numberRangeData`, the CLI validates that `value` is within `[min, max]` and lands on the interval grid (`min + k·interval`); off-range or off-step defaults are rejected.\n\n**Config example:**\n```json\n{ \"name\": \"gap\", \"displayName\": \"Gap\", \"type\": \"NUMBER_RANGE\", \"numberRangeData\": { \"min\": 0, \"max\": 100, \"interval\": 5, \"unit\": \"px\" }, \"defaultValue\": { \"value\": 20, \"unit\": \"px\" } }\n```\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name gap --displayName Gap --type NUMBER_RANGE --numberRangeData '{\"min\":0,\"max\":100,\"interval\":5,\"unit\":\"px\"}' --defaultValue '{\"value\":20,\"unit\":\"px\"}'\n```\n\n**Update the config or default later** (send a new `--numberRangeData` to change it; `--numberRangeData none` is rejected while the prop stays NUMBER_RANGE, since min/max are required):\n```bash\nnpx ikas-component config update-prop --component MyComp --prop gap --numberRangeData '{\"min\":0,\"max\":200,\"interval\":10,\"unit\":\"px\"}'\n```\n\n### PRODUCT_ATTRIBUTE and PRODUCT_ATTRIBUTE_LIST props (product custom fields)\n\n**CRITICAL — these props do NOT hand you attribute values.** What your component receives is a *wrapper* pairing the picker selection with the product it resolves against. Reading `prop.value` (or mapping over the prop) references fields that do not exist at runtime: `check` and `build` stay clean, every field is `undefined`, and the component silently renders nothing.\n\nThree distinct shapes are involved — do not confuse them:\n\n| Where | Shape |\n|-------|-------|\n| Value stored by the editor (visible via `get_section_values`) | `{ \"attributeId\": \"…\", \"usePageData\": true }` — the list variant uses `attributeIds: [...]` |\n| Runtime prop your component receives | `IkasProductAttributeDetail` = `{ product, attributePropValue }` — the list variant is `IkasProductAttributeList` = `{ product, attributeListPropValue }` |\n| Resolved attribute values | Reachable ONLY through the helpers below: `getAttributeDetailValues()` → `IkasProductAttributeValue[]`, `getAttributeListValues()` → `IkasProductAttributeMap[]` (each entry is `{ attribute, values }`) |\n\n**Read them with the helpers from `@ikas/bp-storefront`:**\n```tsx\nimport { getAttributeDetailValues, getAttributeListValues } from \"@ikas/bp-storefront\";\n\n// PRODUCT_ATTRIBUTE → attribute: IkasProductAttributeDetail | null\nif (!attribute?.attributePropValue?.attributeId) return null;\nconst html = getAttributeDetailValues(attribute)\n .map((v) => v.value)\n .filter(Boolean)\n .join(\"\");\n\n// PRODUCT_ATTRIBUTE_LIST → attributeList: IkasProductAttributeList\n// getAttributeListValues returns IkasProductAttributeMap[] — grouped per attribute, NOT a flat value list\nconst groups = getAttributeListValues(attributeList);\ngroups.forEach((g) => console.log(g.attribute.name, g.values));\n```\n\n**Second trap — a resolved value's `value` is often `null`.** `getAttributeDetailValues()` returning items does NOT mean you have text to render. For the choice-style attribute types (`CHOICE`, `MULTIPLE_CHOICE`, `COLOR_THUMBNAIL_MULTIPLE_CHOICE`) the selection lives in `productAttributeOptionId` / `productAttributeOption` and `value` is `null` by design, so `.map((v) => v.value)` yields empty strings and the component looks broken again. Read the label defensively:\n\n```tsx\nconst text = getAttributeDetailValues(attribute)\n .map((v) => v.value ?? v.productAttributeOption?.name ?? \"\")\n .filter(Boolean)\n .join(\", \");\n```\n\nWhich field carries the content depends on `v.productAttribute?.type`: `TEXT`/`NUMERIC`/`HTML`/`DATE`/`DATETIME`/`BOOLEAN` → `value`; the choice types → `productAttributeOption.name`; `IMAGE` → `images` / `imageIds`; `TABLE` → parse with `getIkasProductAttributeTableValue(v)`; `PRODUCT` → `getIkasProductAttrributeProductIds(v)` (that export name really is misspelled — see `get_framework_guide(\"imports\")`). Check the type with `get_type_definition(\"IkasProductAttributeValue\")` before assuming a field is populated.\n\n`PRODUCT_ATTRIBUTE_LIST` is a **single object, not an array** — `attributeList.map(...)` throws at runtime; the selected ids live at `attributeList.attributeListPropValue.attributeIds`. Both prop types are merchant-data types, so neither can carry a `defaultValue`. Details: `get_function_doc(\"getAttributeDetailValues\")`, `get_type_definition(\"IkasProductAttributeDetail\")`, `get_type_definition(\"IkasProductAttributeList\")`.\n\n### LINK and LIST_OF_LINK props (navigation links)\n\n**CRITICAL — the authored `defaultValue` shape is NOT the runtime read shape.** In component code you READ a link as `props.myLink.href` / `.label` / `.subLinks`. But the `defaultValue` you author in `ikas.config.json` MUST be the typed object shown below. Do NOT pass `{ \"href\": \"…\", \"label\": \"…\" }` (legacy shape) and do NOT pass the value as a JSON string — the CLI rejects both.\n\n**LINK** `defaultValue` is a single link object:\n```json\n{ \"linkType\": \"EXTERNAL\", \"label\": \"Shop now\", \"externalLink\": \"https://example.com\", \"subLinks\": [] }\n```\nor, to link to a store page:\n```json\n{ \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] }\n```\n\n**LIST_OF_LINK** `defaultValue` wraps an array of link objects in `{ \"links\": [...] }`:\n```json\n{ \"links\": [\n { \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] },\n { \"linkType\": \"EXTERNAL\", \"label\": \"Blog\", \"externalLink\": \"https://example.com/blog\", \"subLinks\": [] }\n] }\n```\n\n**Rules for every link object:**\n- `linkType` is REQUIRED and must be one of `PAGE` or `EXTERNAL`. `FILE` links are NOT authorable in a code-component default (they reference a store-specific uploaded file) — the CLI rejects them.\n- Each link MUST carry a destination for its `linkType` — the CLI rejects a link that points nowhere. `EXTERNAL` → a non-empty `externalLink` URL. `PAGE` → a `pageType` (e.g. `INDEX`); do NOT invent a `pageId` — it is per-store and filled automatically when the theme is installed, so author only `pageType`.\n- `subLinks` must be an array (use `[]` when empty); each entry is itself a link object with the same shape (for dropdown/nested menus).\n- NEVER use a JSON string, and NEVER use the legacy `{ label, href }` shape.\n\n**Config example (LINK):**\n```json\n{ \"name\": \"ctaLink\", \"displayName\": \"CTA Link\", \"type\": \"LINK\", \"defaultValue\": { \"linkType\": \"EXTERNAL\", \"label\": \"Shop now\", \"externalLink\": \"https://example.com\", \"subLinks\": [] } }\n```\n\n**Config example (LIST_OF_LINK):**\n```json\n{ \"name\": \"menuLinks\", \"displayName\": \"Menu Links\", \"type\": \"LIST_OF_LINK\", \"defaultValue\": { \"links\": [ { \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] } ] } }\n```\n\n**CLI command (LIST_OF_LINK):**\n```bash\nnpx ikas-component config add-prop --component MyComp --name menuLinks --displayName \"Menu Links\" --type LIST_OF_LINK --defaultValue '{\"links\":[{\"linkType\":\"PAGE\",\"label\":\"Home\",\"pageType\":\"INDEX\",\"subLinks\":[]}]}'\n```\n\n### Prop grouping\nProps can be assigned to groups via `groupId` for organized editor sidebar display. See `get_framework_guide(\"prop-groups\")` for details.\n\n### Conditional visibility (show a prop only when it applies)\nAny prop — and any prop group — can be gated on another `BOOLEAN` or `ENUM` prop of the same component via `requiredProp`, so the editor panel shows only the settings that apply to the merchant's current choices (an `Enable badge` toggle revealing the badge props, a `layout` enum revealing `columns` only for `grid`). A gated prop that is `required` stops blocking a publish while it is hidden, and its value is still stored, passed to the component and published. Full contract, rules and CLI flags: `get_framework_guide(\"conditional-visibility\")`.\n\n### Style Props: TYPE and ENUM\n\nThere are two prop types for style values:\n\n- **TYPE** — Structured types with numeric values and units (padding, margin, border-radius, sizes, etc.). Uses `typeId`.\n- **ENUM** — Enum types rendered as dropdown selectors (flex-direction, justify-content, align-items, etc.). Uses `enumTypeId`.\n\nBoth are available for components and sections. For sections, TYPE props are limited to a whitelist of style types. Use `list-types --component-type section` to see section-allowed types.\n\n### TYPE prop (structured types)\nThe `TYPE` prop lets you use structured storefront types like PaddingStyleType, MarginStyleType, SizeStyleType, etc. Available for both components and sections (sections have a restricted whitelist of style types).\n\n**Workflow:**\n1. Run `npx ikas-component config list-types` to get available types (requires dev server running with editor connected)\n2. Use the `typeId` from the output when adding the prop\n3. For sections: `npx ikas-component config list-types --component-type section` to see only section-allowed types\n\n**Example config:**\n```json\n{\n \"name\": \"spacing\",\n \"displayName\": \"Spacing\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-PaddingStyleType\"\n}\n```\n\n**Providing a default value:** Style TYPE props accept a `defaultValue` object — size/style types (Padding, Margin, Width, Height, Gap, BorderRadius, Size, …) take `{ \"value\": <number>, \"unit\": \"px\" | \"rem\" | \"%\" | \"vh\" | \"vw\" }` (or `{ \"css\": \"<raw css>\" }`). Add it to the prop object, e.g. `\"defaultValue\": { \"value\": 16, \"unit\": \"px\" }`. For an `_array` typeId the default is an array of these objects; the `--defaultValue` flag also accepts this JSON as a string. Only `css`, `value` and `unit` keys are allowed on a size/style default object — any other field is rejected.\n\n**Array example** (append `_array` to typeId):\n```json\n{\n \"name\": \"margins\",\n \"displayName\": \"Margins\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginStyleType_array\"\n}\n```\nThis generates `margins?: MarginStyleType[]` in types.ts.\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name spacing --displayName Spacing --type TYPE --typeId \"@ikas/bp-storefront-models-PaddingStyleType\"\n```\n\n### ENUM prop (enum style types)\nThe `ENUM` prop lets you use enum-based types that render as dropdown selectors. There are two kinds of enum types:\n\n1. **Built-in enums** (prefix `@ikas/`): FlexDirectionStyleType, JustifyContentStyleType, AlignItemsStyleType, etc. Available from `list-types` when editor is connected.\n2. **Custom enums**: Created with `config add-enum`. Works offline, no editor needed.\n3. **Font Style enum** (sentinel `__patternElementEnum__`): lets store owners pick one of the theme's typography tokens. Built-in — no `add-enum` and no editor connection needed. See \"Font Style enum (typography picker)\" below.\n\n**IMPORTANT:** `add-prop --type ENUM` validates that the `enumTypeId` references an existing enum. You MUST create custom enums first with `config add-enum` before using them. The command will reject unknown enum IDs.\n\n**Custom enum workflow (recommended for AI-driven generation):**\n```bash\n# Step 1: Create the enum FIRST\nnpx ikas-component config add-enum --name \"AspectRatio\" --options '{\"Square\":\"1/1\",\"Landscape\":\"16/9\",\"Portrait\":\"3/4\"}'\n# Returns: {\"success\":true,\"enumId\":\"aBcDeFgHiJ\",...}\n\n# Step 2: Use the returned enumId in add-prop\nnpx ikas-component config add-prop --component MyComp --name aspectRatio --displayName \"Aspect Ratio\" --type ENUM --enumTypeId aBcDeFgHiJ\n```\n\n**Built-in enum workflow:**\n1. Run `npx ikas-component config list-types` — enum types have `category: \"enum\"` in the output\n2. Use the `enumTypeId` from the output when adding the prop\n\n**Example config (built-in enum):**\n```json\n{\n \"name\": \"direction\",\n \"displayName\": \"Direction\",\n \"type\": \"ENUM\",\n \"enumTypeId\": \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n}\n```\nThis generates `direction?: string` in types.ts.\n\n**CLI command (built-in enum):**\n```bash\nnpx ikas-component config add-prop --component MyComp --name direction --displayName Direction --type ENUM --enumTypeId \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n```\n\n**Note:** `list-types` requires dev server with editor connected. Custom enums (`config add-enum`) work offline.\n\n### Font Style enum (typography picker)\nUse the built-in sentinel `enumTypeId: \"__patternElementEnum__\"` to let store owners pick one of the theme's typography tokens (a \"Font Style\" prop). It works exactly like a built-in enum: no `config add-enum` call and no editor connection required.\n\n**Example config:**\n```json\n{\n \"name\": \"fontStyle\",\n \"displayName\": \"Font Style\",\n \"type\": \"ENUM\",\n \"enumTypeId\": \"__patternElementEnum__\",\n \"required\": false\n}\n```\nThis generates `fontStyle?: string` in types.ts.\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name fontStyle --displayName \"Font Style\" --type ENUM --enumTypeId __patternElementEnum__\n```\n\n**Runtime:** the prop value is the chosen typography's **id** (a plain string), not the typography object itself. Resolve it with `getThemeTypography()` (see `get_framework_guide(\"theme-globals\")`) and apply its `className`:\n```tsx\nimport { getThemeTypography } from \"@ikas/bp-storefront\";\nimport type { Props } from \"./types\"; // fontStyle?: string\n\nexport function MyCard({ fontStyle }: Props) {\n const t = getThemeTypography().find((t) => t.id === fontStyle);\n return <p className={t?.className}>...</p>; // _<id> class applies the theme's typography CSS\n}\n```",
39
42
  "tags": [
40
43
  "props",
41
44
  "types",
@@ -44,6 +47,8 @@
44
47
  "RICH_TEXT",
45
48
  "NUMBER",
46
49
  "NUMBER_RANGE",
50
+ "numberRangeData",
51
+ "IkasNumberRange",
47
52
  "BOOLEAN",
48
53
  "VIDEO",
49
54
  "DATE",
@@ -54,6 +59,13 @@
54
59
  "COLOR",
55
60
  "PRODUCT",
56
61
  "PRODUCT_ATTRIBUTE",
62
+ "PRODUCT_ATTRIBUTE_LIST",
63
+ "IkasProductAttributeDetail",
64
+ "IkasProductAttributeList",
65
+ "getAttributeDetailValues",
66
+ "getAttributeListValues",
67
+ "attributePropValue",
68
+ "attributeListPropValue",
57
69
  "CATEGORY",
58
70
  "BRAND",
59
71
  "BLOG",
@@ -61,13 +73,15 @@
61
73
  "TYPE",
62
74
  "typeId",
63
75
  "ENUM",
64
- "enumTypeId"
76
+ "enumTypeId",
77
+ "requiredProp",
78
+ "conditional visibility"
65
79
  ]
66
80
  },
67
81
  "component-structure": {
68
82
  "title": "Component Structure",
69
83
  "description": "How to write a Preact component for ikas",
70
- "content": "Each component consists of three files. There are two patterns: **components** (child elements) and **sections** (page-level containers).\n\n## Component Pattern (child elements like buttons, cards, badges)\n\n### index.tsx - Component Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport function MyComponent({ title, showButton }: Props) {\n return (\n <div className=\"my-component\">\n <h1>{title}</h1>\n {showButton && <button>Click me</button>}\n </div>\n );\n}\n\nexport default MyComponent;\n```\n\n### types.ts - Props Interface\n```typescript\nexport interface Props {\n title: string; // TEXT prop -> string\n showButton?: boolean; // optional BOOLEAN prop\n count: number; // NUMBER prop -> number\n}\n```\n\n### styles.css\n```css\n.my-component {\n padding: 16px;\n}\n\n.my-component h1 {\n font-size: 24px;\n}\n```\n\n## Section Pattern (page-level containers like headers, hero banners, footers)\n\nSections use a `<section>` root element, full-width styling, and a `Props` interface. In `ikas.config.json` they have `\"type\": \"section\"`. **Every section MUST include a `backgroundColor` COLOR prop** (default: `#ffffff`) so store owners can customize the section background. Consider also adding `textColor` COLOR props for text elements sitting directly on the section background, so they remain readable when the background changes.\n\n### index.tsx - Section Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport function HeroBanner({ heading, subtitle, backgroundColor }: Props) {\n return (\n <section className=\"hero-banner\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"hero-banner-inner\">\n <h2>{heading}</h2>\n {subtitle && <p>{subtitle}</p>}\n </div>\n </section>\n );\n}\n\nexport default HeroBanner;\n```\n\n### types.ts - Section Props Interface\n```typescript\nexport interface Props {\n heading: string;\n subtitle?: string;\n backgroundColor?: string;\n}\n```\n\n### styles.css - Full-width section styles\n```css\n.hero-banner {\n width: 100%;\n padding: 64px 24px;\n}\n\n.hero-banner-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n```\n\n## Key Rules\n- Export the component as both a named export and a default export: `export function X() {...}` + `export default X;`\n- Use Preact (not React) - but JSX syntax is the same\n- Import types from `./types` for props\n- Use `className` not `class` for CSS classes\n- Storefront functions and types come from `@ikas/bp-storefront`\n- CSS classes are automatically scoped to your component at build time. Use plain CSS class selectors - they won't conflict with other components or the page.\n- **All user-visible text MUST be TEXT props** — never hardcode strings in JSX. This ensures all text is translatable. Use `defaultValue` in config for English defaults.\n- **Sections MUST include a `backgroundColor` COLOR prop** (default: `#ffffff`). Apply via `style={backgroundColor ? { backgroundColor } : undefined}` on the root `<section>` element. Optionally add `textColor` for text directly on the section background.\n\n## Root Components Are Automatically Reactive\n\nThe ikas runtime wraps every root component render in a MobX `autorun()`, so **root exports are already reactive** — they automatically re-render when any MobX store they read (`cartStore`, `customerStore`, etc.) changes. You do **not** need `observer()` on root component exports.\n\n### Correct: Plain root export reading stores\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function CartSummary({ title }: Props) {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return (\n <section className=\"cart-summary\">\n <h2>{title}</h2>\n <p>{itemCount} items in cart</p>\n </section>\n );\n}\n\nexport default CartSummary;\n```\n\n### Anti-pattern: Do NOT wrap root exports with observer\n```tsx\n// WRONG — observer() is redundant on root components\nconst CartSummary = observer(function CartSummary({ title }: Props) {\n ...\n});\nexport default CartSummary;\n```\n\n## Using observer for Sub-Components\n\nWhen you extract a **sub-component** that independently reads MobX stores, wrap it with `observer()` from `@ikas/component-utils` so it re-renders when store data changes.\n\n### When to use observer\n- A **sub-component** (not the root export) reads `cartStore.cart`, `customerStore.customer`, or any other MobX observable\n- You extract part of a component into a separate function that needs independent reactivity\n\n### When observer is NOT needed\n- **Root component exports** — the ikas runtime handles reactivity via `autorun()`\n- Components that only use props passed from parent — no store reads\n- Static components with no reactive data\n\n### Example: Root export with observer sub-component\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n\n// Root export: NO observer needed — autorun() handles reactivity\nexport function Header({ logo }: Props) {\n return (\n <header>\n <img src={logo} alt=\"Logo\" />\n <CartBadge />\n </header>\n );\n}\n\nexport default Header;\n```\n\n**Important:** When using `observer` on sub-components, define the component as a named function expression (not arrow function) and assign it to a `const`. This ensures proper display names in React DevTools.\n\n## Sub-Component File Organization\n\nWhen you extract sub-components, **ALWAYS** place them in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. Never create flat `.tsx` files inside a component folder. See `get_framework_guide(\"sub-component-patterns\")` for the full pattern, directory structure, CSS @import rules, and examples.",
84
+ "content": "Each component consists of three files. There are two patterns: **components** (child elements) and **sections** (page-level containers).\n\n## Component Pattern (child elements like buttons, cards, badges)\n\n### index.tsx - Component Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport function MyComponent({ title, showButton }: Props) {\n return (\n <div className=\"my-component\">\n <h1>{title}</h1>\n {showButton && <button>Click me</button>}\n </div>\n );\n}\n\nexport default MyComponent;\n```\n\n### types.ts - Props Interface\n```typescript\nexport interface Props {\n title: string; // TEXT prop -> string\n showButton?: boolean; // optional BOOLEAN prop\n count: number; // NUMBER prop -> number\n}\n```\n\n### styles.css\n```css\n.my-component {\n padding: 16px;\n}\n\n.my-component h1 {\n font-size: 24px;\n}\n```\n\n## Section Pattern (page-level containers like headers, hero banners, footers)\n\nSections use a `<section>` root element, full-width styling, and a `Props` interface. In `ikas.config.json` they have `\"type\": \"section\"`. **Every section MUST include a `backgroundColor` COLOR prop** (default: `#ffffff`) so store owners can customize the section background. Consider also adding `textColor` COLOR props for text elements sitting directly on the section background, so they remain readable when the background changes.\n\n### index.tsx - Section Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport function HeroBanner({ heading, subtitle, backgroundColor }: Props) {\n return (\n <section className=\"hero-banner\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"hero-banner-inner\">\n <h2>{heading}</h2>\n {subtitle && <p>{subtitle}</p>}\n </div>\n </section>\n );\n}\n\nexport default HeroBanner;\n```\n\n### types.ts - Section Props Interface\n```typescript\nexport interface Props {\n heading: string;\n subtitle?: string;\n backgroundColor?: string;\n}\n```\n\n### styles.css - Full-width section styles\n```css\n.hero-banner {\n width: 100%;\n padding: 64px 24px;\n}\n\n.hero-banner-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n```\n\n## Key Rules\n- Export the component as both a named export and a default export: `export function X() {...}` + `export default X;`\n- Use Preact (not React) - but JSX syntax is the same\n- Import types from `./types` for props\n- Use `className` not `class` for CSS classes\n- Storefront functions and types come from `@ikas/bp-storefront`\n- CSS classes are automatically scoped to your component at build time. Use plain CSS class selectors - they won't conflict with other components or the page.\n- **All user-visible text MUST be TEXT props** — never hardcode strings in JSX. This ensures all text is translatable. Use `defaultValue` in config for English defaults.\n- **Sections MUST include a `backgroundColor` COLOR prop** (default: `#ffffff`). Apply via `style={backgroundColor ? { backgroundColor } : undefined}` on the root `<section>` element. Optionally add `textColor` for text directly on the section background.\n\n## Root Components Are Automatically Reactive\n\nThe ikas runtime wraps every root component render in a MobX `autorun()`, so **root exports are already reactive** — they automatically re-render when any MobX store they read (`cartStore`, `customerStore`, etc.) changes. You do **not** need `observer()` on root component exports.\n\n### Correct: Plain root export reading stores\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function CartSummary({ title }: Props) {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return (\n <section className=\"cart-summary\">\n <h2>{title}</h2>\n <p>{itemCount} items in cart</p>\n </section>\n );\n}\n\nexport default CartSummary;\n```\n\n### Anti-pattern: Do NOT wrap root exports with observer\n```tsx\n// WRONG — observer() is redundant on root components\nconst CartSummary = observer(function CartSummary({ title }: Props) {\n ...\n});\nexport default CartSummary;\n```\n\n## Using observer for Sub-Components\n\nWhen you extract a **sub-component** that independently reads MobX stores, wrap it with `observer()` from `@ikas/component-utils` so it re-renders when store data changes.\n\n### When to use observer\n- A **sub-component** (not the root export) reads `cartStore.cart`, `customerStore.customer`, or any other MobX observable\n- You extract part of a component into a separate function that needs independent reactivity\n\n### When observer is NOT needed\n- **Root component exports** — the ikas runtime handles reactivity via `autorun()`\n- Components that only use props passed from parent — no store reads\n- Static components with no reactive data\n\n### Example: Root export with observer sub-component\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n\n// Root export: NO observer needed — autorun() handles reactivity\nexport function Header({ logo }: Props) {\n return (\n <header>\n <img src={logo} alt=\"Logo\" />\n <CartBadge />\n </header>\n );\n}\n\nexport default Header;\n```\n\n**Important:** When using `observer` on sub-components, define the component as a named function expression (not arrow function) and assign it to a `const`. This ensures proper display names in React DevTools.\n\n## Sub-Component File Organization\n\nWhen you extract sub-components, **ALWAYS** place them in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. Never create flat `.tsx` files inside a component folder. See `get_framework_guide(\"sub-component-patterns\")` for the full pattern, directory structure, CSS collection rules, and examples.",
71
85
  "tags": [
72
86
  "component",
73
87
  "preact",
@@ -85,7 +99,7 @@
85
99
  "build-system": {
86
100
  "title": "Build System",
87
101
  "description": "How components are built and bundled",
88
- "content": "The build system uses Vite + esbuild to compile components.\n\n### Development\n```bash\nnpm run dev\n```\nStarts a dev server that:\n- Watches for file changes\n- Compiles components on the fly\n- Communicates with the ikas editor via WebSocket\n- Provides live updates in the editor preview\n\n### Production Build\n```bash\nnpm run build\n```\nBuilds optimized bundles:\n- Creates separate server and client bundles\n- Server bundle: used for SSR (no browser APIs)\n- Client bundle: used for hydration and interactivity\n- CSS is extracted and scoped per component\n- Tree-shakes unused code\n\n### What happens during build:\n1. TypeScript is compiled to JavaScript\n2. JSX is transformed (Preact's h function)\n3. CSS is scoped with unique prefixes\n4. Code is bundled per-component\n5. Externals (`preact`, `@ikas/component-utils`) are not bundled\n\n### Publishing\n```bash\nnpm run publish\n```\nUploads the built components to the ikas platform.",
102
+ "content": "The build system uses Vite + esbuild to compile components.\n\n### Development\n```bash\nikas theme dev\n```\nStarts a dev server that:\n- Watches for file changes\n- Compiles components on the fly\n- Communicates with the ikas editor via WebSocket (through a tunnel registered by the ikas CLI — see the dev-server guide)\n- Provides live updates in the editor preview\n\n### Production Build\n```bash\nnpm run build\n```\nBuilds optimized bundles:\n- Creates separate server and client bundles\n- Server bundle: used for SSR (no browser APIs)\n- Client bundle: used for hydration and interactivity\n- CSS is extracted and scoped per component\n- Tree-shakes unused code\n\n### What happens during build:\n1. TypeScript is compiled to JavaScript\n2. JSX is transformed (Preact's h function)\n3. CSS is scoped with unique prefixes\n4. Code is bundled per-component\n5. Externals (`preact`, `@ikas/component-utils`) are not bundled\n\n### Publishing\n```bash\nnpm run publish\n```\nUploads the built components to the ikas platform.",
89
103
  "tags": [
90
104
  "build",
91
105
  "vite",
@@ -100,20 +114,13 @@
100
114
  "css-scoping": {
101
115
  "title": "CSS Scoping Rules",
102
116
  "description": "How CSS is scoped to prevent conflicts between components",
103
- "content": "CSS files referenced in `ikas.config.json` are automatically scoped at build time.\n\n### How it works:\n- Each component's CSS selectors are prefixed with a unique scope identifier\n- `.my-class` becomes `.[scope]-my-class` in the output\n- This prevents style conflicts between different components on the same page\n\n### Best practices:\n- Use class selectors (`.my-class`) rather than element selectors (`div`, `h1`)\n- Avoid using `!important` - scoping handles isolation\n- Don't use global selectors or `@global` - they won't be scoped\n- Keep CSS in the component's `styles.css` file referenced in config\n- Nested selectors work: `.parent .child` both get scoped\n\n### Example:\n```css\n/* What you write */\n.product-card {\n padding: 16px;\n border: 1px solid #eee;\n}\n\n.product-card .title {\n font-size: 18px;\n font-weight: bold;\n}\n\n.product-card .price {\n color: #e53e3e;\n}\n```\n\nThe build system transforms this so your `.product-card`, `.title`, and `.price` classes are unique to your component and won't clash with any other component or theme styles.\n\n### Global CSS Exception\n\nThe one intentional exception to scoping is **Global CSS** — project-wide styles defined in `src/global.css` that are loaded unscoped before any component CSS. Global CSS is ideal for CSS custom properties (variables), resets, and shared utility classes. The loading order is: **Global CSS → Shared chunk CSS → Component CSS**, so global styles have the lowest specificity and component styles always win.\n\nSee `get_framework_guide(\"global-css\")` for full details on global CSS setup and usage.",
104
- "tags": [
105
- "css",
106
- "scoping",
107
- "styles",
108
- "isolation",
109
- "classes",
110
- "global"
111
- ]
117
+ "content": "CSS files referenced in `ikas.config.json` are automatically scoped at build time.\n\n### How it works:\n- Each component's CSS selectors are prefixed with a unique scope identifier\n- `.my-class` becomes `.[scope]-my-class` in the output\n- This prevents style conflicts between different components on the same page\n\n### Best practices:\n- Use class selectors (`.my-class`) rather than element selectors (`div`, `h1`)\n- Avoid using `!important` - scoping handles isolation\n- Don't use global selectors or `@global` - they won't be scoped\n- Keep CSS in the component's `styles.css` file referenced in config, or in a sub-component's sibling `styles.css` — both are collected and scoped automatically on `dev`/`publish`\n- Nested selectors work: `.parent .child` both get scoped\n\n### Example:\n```css\n/* What you write */\n.product-card {\n padding: 16px;\n border: 1px solid #eee;\n}\n\n.product-card .title {\n font-size: 18px;\n font-weight: bold;\n}\n\n.product-card .price {\n color: #e53e3e;\n}\n```\n\nThe build system transforms this so your `.product-card`, `.title`, and `.price` classes are unique to your component and won't clash with any other component or theme styles.\n\n### Global CSS Exception\n\nThe one intentional exception to component scoping is **Global CSS** — project-wide styles defined in `src/global.css`. It gets no `.cc_{componentId}` prefix, but it is still isolated to your project: every rule is wrapped in `:where([data-cc-scope~=\"<projectId>\"])`, and `@font-face` / `@keyframes` / `@counter-style` names declared there are prefixed with `cc_<projectId>_` (references in component CSS are rewritten to match automatically). Global CSS is ideal for CSS custom properties (variables), fonts, resets, and shared utility classes. The loading order is: **Global CSS → Shared chunk CSS → Component CSS**, so global styles have the lowest specificity and component styles always win.\n\nSee `get_framework_guide(\"global-css\")` for full details on global CSS setup and usage.",
118
+ "tags": ["css", "scoping", "styles", "isolation", "classes", "global"]
112
119
  },
113
120
  "global-css": {
114
121
  "title": "Global CSS",
115
- "description": "Project-wide unscoped CSS for variables, resets, and shared utilities",
116
- "content": "## Global CSS\n\nGlobal CSS lets you define project-wide styles that are shared across all components — without scoping. This is ideal for:\n\n- **CSS custom properties (variables)** — colors, spacing, typography tokens\n- **CSS resets** — normalize/reset base styles\n- **Base typography** — font-family, line-height defaults\n- **Utility classes** — common layout helpers\n\n### Setup\n\n1. Create a `src/global.css` file in your project root:\n\n```css\n/* src/global.css */\n:root {\n --color-primary: #2563eb;\n --color-secondary: #7c3aed;\n --color-text: #1f2937;\n --color-text-muted: #6b7280;\n --color-border: #e5e7eb;\n --color-background: #ffffff;\n\n --spacing-xs: 4px;\n --spacing-sm: 8px;\n --spacing-md: 16px;\n --spacing-lg: 24px;\n --spacing-xl: 48px;\n\n --font-sans: system-ui, -apple-system, sans-serif;\n --font-mono: ui-monospace, monospace;\n\n --radius-sm: 4px;\n --radius-md: 8px;\n --radius-lg: 16px;\n}\n```\n\n2. Add the `globalStyles` field to `ikas.config.json`:\n\n```json\n{\n \"name\": \"my-project\",\n \"version\": \"1.0.0\",\n \"globalStyles\": \"./src/global.css\",\n \"components\": [...]\n}\n```\n\n### Using Global CSS Variables in Components\n\nOnce defined in `global.css`, CSS custom properties are available in any component's `styles.css`:\n\n```css\n/* src/components/ProductCard/styles.css */\n.product-card {\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n padding: var(--spacing-md);\n font-family: var(--font-sans);\n}\n\n.product-card .title {\n color: var(--color-text);\n}\n\n.product-card .price {\n color: var(--color-primary);\n}\n```\n\n### Loading Order\n\nCSS is loaded in this order, from lowest to highest specificity:\n\n1. **Global CSS** (`global.css`) — unscoped, loaded first\n2. **Shared chunk CSS** — scoped styles from shared internal components\n3. **Component CSS** (`styles.css`) — scoped per-component styles\n\nBecause global CSS loads first and is unscoped, component-scoped styles will always override global styles when selectors conflict. This means you can safely define base defaults in `global.css` and override them in component `styles.css` files.\n\n### Key Differences from Component CSS\n\n| | Global CSS | Component CSS |\n|---|---|---|\n| **File** | `src/global.css` | `src/components/X/styles.css` |\n| **Scoping** | NOT scoped (no `.cc_` prefix) | Scoped with `.cc_{componentId}` prefix |\n| **Config** | `globalStyles` in ikas.config.json | `styles` on each component |\n| **Purpose** | CSS variables, resets, utilities | Component-specific styles |\n| **Load order** | First (lowest specificity) | Last (highest specificity) |\n\n### Best Practices\n\n- **Use CSS custom properties** for design tokens (colors, spacing, fonts) — they cascade naturally and components can reference them\n- **Keep global CSS minimal** — only put truly shared values here. Component-specific styles belong in the component's `styles.css`\n- **Avoid class selectors in global CSS** that target component internals — use custom properties instead for themeable values\n- **Don't duplicate** — if a style only applies to one component, put it in that component's `styles.css`\n\n### Project Structure with Global CSS\n\n```\nmy-project/\n├── src/\n│ ├── global.css # Project-wide unscoped CSS\n│ └── components/\n│ ├── ProductCard/\n│ │ ├── index.tsx\n│ │ ├── types.ts\n│ │ └── styles.css # Uses var(--color-primary) etc.\n│ └── index.ts\n├── ikas.config.json # Has globalStyles: \"./src/global.css\"\n├── package.json\n└── tsconfig.json\n```\n\nSee `get_framework_guide(\"css-scoping\")` for details on how component CSS scoping works.",
122
+ "description": "Project-wide CSS (scoped to your project) for variables, fonts, resets, and shared utilities",
123
+ "content": "## Global CSS\n\nGlobal CSS lets you define project-wide styles that are shared across all components of your project. It is isolated per project at build time (every rule is wrapped in `:where([data-cc-scope~=\"<projectId>\"])`), but not per component. This is ideal for:\n\n- **CSS custom properties (variables)** — colors, spacing, typography tokens\n- **CSS resets** — normalize/reset base styles\n- **Base typography** — font-family, line-height defaults\n- **Utility classes** — common layout helpers\n\n### Setup\n\n1. Create a `src/global.css` file in your project root:\n\n```css\n/* src/global.css */\n:root {\n --color-primary: #2563eb;\n --color-secondary: #7c3aed;\n --color-text: #1f2937;\n --color-text-muted: #6b7280;\n --color-border: #e5e7eb;\n --color-background: #ffffff;\n\n --spacing-xs: 4px;\n --spacing-sm: 8px;\n --spacing-md: 16px;\n --spacing-lg: 24px;\n --spacing-xl: 48px;\n\n --font-sans: system-ui, -apple-system, sans-serif;\n --font-mono: ui-monospace, monospace;\n\n --radius-sm: 4px;\n --radius-md: 8px;\n --radius-lg: 16px;\n}\n```\n\n2. Add the `globalStyles` field to `ikas.config.json`:\n\n```json\n{\n \"name\": \"my-project\",\n \"version\": \"1.0.0\",\n \"globalStyles\": \"./src/global.css\",\n \"components\": [...]\n}\n```\n\n### Using Global CSS Variables in Components\n\nOnce defined in `global.css`, CSS custom properties are available in any component's `styles.css`:\n\n```css\n/* src/components/ProductCard/styles.css */\n.product-card {\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n padding: var(--spacing-md);\n font-family: var(--font-sans);\n}\n\n.product-card .title {\n color: var(--color-text);\n}\n\n.product-card .price {\n color: var(--color-primary);\n}\n```\n\n### Fonts, Keyframes and Counter Styles\n\n`@font-face`, `@keyframes` and `@counter-style` names live in a document-global namespace, so the build (editor preview and publish alike) prefixes every name declared in `global.css` with `cc_<projectId>_` — `\"Tanker\"` becomes `\"cc_<projectId>_Tanker\"`. You keep writing the plain name; references are rewritten to match automatically in:\n\n- component and shared-chunk CSS: `font`, `font-family`, `animation`, `animation-name`, `list-style`, `list-style-type`, `counter()` / `counters()`\n- CSS custom properties, for **font names only**: `--font-display: \"Tanker\", serif` is rewritten, so `font-family: var(--font-display)` works\n\n```css\n/* src/global.css */\n@font-face {\n font-family: \"Tanker\";\n /* absolute URL — local font files are not shipped with the project */\n src: url(\"https://cdn.example.com/tanker.woff2\") format(\"woff2\");\n font-display: swap;\n}\n\n:root {\n --font-display: \"Tanker\", Georgia, serif;\n}\n```\n\n```css\n/* src/components/Hero/styles.css */\n.hero-headline {\n font-family: var(--font-display); /* or: font-family: \"Tanker\", Georgia, serif; */\n}\n```\n\nNot rewritten — avoid these for names declared in `global.css`:\n\n- inline JS styles (`style={{ fontFamily: \"Tanker\" }}`) — use a class instead\n- keyframe / counter-style names inside custom properties (`--anim: fade`) — reference them from `animation` / `list-style` declarations directly\n- top-level `body { }` / `html { }` rules — they are dropped; style the elements you render (`.hero`, `img`, `[data-x]`)\n\nFonts that come from an external stylesheet (`@import url(\"https://fonts.googleapis.com/...\")` as the first rule of `global.css`) or from the theme's typography tokens (see `get_framework_guide(\"theme-globals\")`) keep their original names and need no rewriting.\n\n### Loading Order\n\nCSS is loaded in this order, from lowest to highest specificity:\n\n1. **Global CSS** (`global.css`) — project-scoped, loaded first\n2. **Shared chunk CSS** — scoped styles from shared internal components (their sibling `styles.css` files, collected automatically)\n3. **Component CSS** (`styles.css`) — scoped per-component styles\n\nBecause global CSS loads first and has the lowest specificity, component-scoped styles will always override global styles when selectors conflict. This means you can safely define base defaults in `global.css` and override them in component `styles.css` files.\n\n### Key Differences from Component CSS\n\n| | Global CSS | Component CSS |\n|---|---|---|\n| **File** | `src/global.css` | `src/components/X/styles.css` |\n| **Scoping** | Scoped per project (`[data-cc-scope]`); no `.cc_{componentId}` prefix | Scoped with `.cc_{componentId}` prefix |\n| **Config** | `globalStyles` in ikas.config.json | `styles` on each component |\n| **Purpose** | CSS variables, resets, utilities | Component-specific styles |\n| **Load order** | First (lowest specificity) | Last (highest specificity) |\n\n### Best Practices\n\n- **Use CSS custom properties** for design tokens (colors, spacing, fonts) — they cascade naturally and components can reference them (font names declared with `@font-face` in `global.css` are rewritten inside custom properties too — see \"Fonts, Keyframes and Counter Styles\")\n- **Keep global CSS minimal** — only put truly shared values here. Component-specific styles belong in the component's `styles.css`\n- **Avoid class selectors in global CSS** that target component internals — use custom properties instead for themeable values\n- **Don't duplicate** — if a style only applies to one component, put it in that component's `styles.css`\n\n### Project Structure with Global CSS\n\n```\nmy-project/\n├── src/\n│ ├── global.css # Project-wide CSS (scoped to the project)\n│ └── components/\n│ ├── ProductCard/\n│ │ ├── index.tsx\n│ │ ├── types.ts\n│ │ └── styles.css # Uses var(--color-primary) etc.\n│ └── index.ts\n├── ikas.config.json # Has globalStyles: \"./src/global.css\"\n├── package.json\n└── tsconfig.json\n```\n\nSee `get_framework_guide(\"css-scoping\")` for details on how component CSS scoping works.",
117
124
  "tags": [
118
125
  "css",
119
126
  "global",
@@ -122,35 +129,36 @@
122
129
  "reset",
123
130
  "utilities",
124
131
  "global.css",
125
- "globalStyles"
132
+ "globalStyles",
133
+ "font-face",
134
+ "fonts",
135
+ "keyframes",
136
+ "counter-style"
126
137
  ]
127
138
  },
128
139
  "dev-server": {
129
140
  "title": "Dev Server Usage",
130
- "description": "How to use the development server with the ikas editor",
131
- "content": "The dev server enables live development with the ikas editor.\n\n### Starting the dev server:\n```bash\nnpm run dev\n```\n\nThis starts a local server (default port 3000) that:\n1. Compiles your components\n2. Opens a WebSocket connection for live reload\n3. Watches file changes and recompiles automatically\n\n### Connecting to the editor:\n1. Open the ikas editor (editor.ikas.com)\n2. Go to your store's theme editor\n3. Open the \"Dev Components\" panel\n4. Enter your dev server URL (e.g., `http://localhost:3000`)\n5. Your components will appear in the component picker\n\n### Development workflow:\n1. Start dev server with `npm run dev`\n2. Connect the editor to your dev server\n3. Add/modify components in your code\n4. Changes appear automatically in the editor\n5. Use the editor to configure prop values and preview\n\n### Adding a new component:\n```bash\nnpm run add\n```\nThis interactive command:\n- Asks for component name\n- Creates the component directory and files\n- Updates `ikas.config.json` with the new component entry",
141
+ "description": "How to start the dev server with `ikas theme dev` and connect it to ikas Studio (the theme editor)",
142
+ "content": "The dev server enables live development with ikas Studio (the theme editor). Studio finds your local dev server through a WebSocket routing record registered by the `ikas` CLI — you never enter a dev server URL manually.\n\n### Official workflow (ikas CLI):\n\n1. Install the ikas CLI globally:\n```bash\nnpm install ikas -g\n```\n\n2. Link your project folder to the editor (create a new project or link an existing one). Copy the exact command from the \"Code Components\" panel in Studio — it includes your editor URL:\n```bash\nikas theme init -e https://{store}.myikas.com/admin/storefront/{storefrontId}/editor/{themeId}\n```\n\n3. Start the dev server from the project folder:\n```bash\nikas theme dev\n```\nThis command:\n- Compiles your components and watches file changes (live reload over WebSocket, local port 5201)\n- Opens a secure tunnel to the local dev server\n- Registers the tunnel's `wss://` URL with ikas so Studio can reach your dev server (the routing stays valid for 8 hours and is cleared when you stop the terminal session)\n\n4. In Studio, open the \"Code Components\" panel and click **Connect**. Your components appear in the component picker.\n\n### Important — do NOT start with `npm run dev`\nRunning the project-local dev server directly (`npm run dev` or `ikas-component dev`) only starts a local server on `127.0.0.1:5201`. It does NOT open the tunnel and does NOT register the WebSocket routing, so Studio cannot find or connect to your dev server. Always start development with `ikas theme dev`.\n\n### Development workflow:\n1. Start the dev server with `ikas theme dev`\n2. Click Connect in Studio's Code Components panel\n3. Add/modify components in your code\n4. Changes appear automatically in the editor\n5. Use the editor to configure prop values and preview\n\n### Adding a new component:\n```bash\nnpm run add\n```\nThis interactive command:\n- Asks for component name\n- Creates the component directory and files\n- Updates `ikas.config.json` with the new component entry",
132
143
  "tags": [
133
144
  "dev",
134
145
  "server",
135
146
  "development",
136
147
  "editor",
148
+ "studio",
137
149
  "live-reload",
138
150
  "websocket",
139
- "workflow"
151
+ "workflow",
152
+ "tunnel",
153
+ "connect",
154
+ "routing"
140
155
  ]
141
156
  },
142
157
  "imports": {
143
158
  "title": "Import Patterns",
144
159
  "description": "How to import storefront functions and types",
145
- "content": "### Storefront Functions & Types\nImport API functions and TypeScript types from `@ikas/bp-storefront`:\n```typescript\nimport {\n // Product functions\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n getProductVariantFormattedFinalPrice,\n \n // Cart functions\n addItemToCart,\n cartStore,\n \n // Navigation\n Router,\n \n // Customer\n customerStore,\n customerLogin,\n hasCustomer,\n \n // Favorites\n addIkasProductToFavorites,\n isFavoriteIkasProduct,\n\n // Type models\n IkasProduct,\n IkasProductVariant,\n IkasCart,\n IkasOrderLineItem,\n IkasCustomer,\n IkasOrder,\n IkasCategory,\n IkasBrand,\n IkasBlog\n} from \"@ikas/bp-storefront\";\n```\n\n### Store Instances\nPre-initialized MobX stores:\n```typescript\nimport { cartStore, customerStore } from \"@ikas/bp-storefront\";\n\n// Cart data\ncartStore.cart?.orderLineItems\n\n// Customer data \ncustomerStore.customer?.email\n```\n\n### Observer (for sub-components with reactive updates)\n```typescript\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span>Cart: {itemCount}</span>;\n});\n\n// Root export: plain function — ikas runtime handles reactivity via autorun()\nexport function MySection({ title }: Props) {\n return (\n <div>\n <h1>{title}</h1>\n <CartBadge />\n </div>\n );\n}\n\nexport default MySection;\n```\n\nDo **NOT** wrap root component exports with `observer()`. The ikas runtime handles root reactivity automatically via `autorun()`. Only wrap **sub-components** with `observer()` when they independently read from stores (`cartStore`, `customerStore`).",
146
- "tags": [
147
- "import",
148
- "storefront",
149
- "models",
150
- "types",
151
- "observer",
152
- "stores"
153
- ]
160
+ "content": "### Storefront Functions & Types\nImport API functions and TypeScript types from `@ikas/bp-storefront`:\n```typescript\nimport {\n // Product functions\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n getProductVariantFormattedFinalPrice,\n \n // Cart functions\n addItemToCart,\n cartStore,\n \n // Navigation\n Router,\n \n // Customer\n customerStore,\n customerLogin,\n hasCustomer,\n \n // Favorites\n addIkasProductToFavorites,\n isFavoriteIkasProduct,\n\n // Type models\n IkasProduct,\n IkasProductVariant,\n IkasCart,\n IkasOrderLineItem,\n IkasCustomer,\n IkasOrder,\n IkasCategory,\n IkasBrand,\n IkasBlog\n} from \"@ikas/bp-storefront\";\n```\n\n### Store Instances\nPre-initialized MobX stores:\n```typescript\nimport { cartStore, customerStore } from \"@ikas/bp-storefront\";\n\n// Cart data\ncartStore.cart?.orderLineItems\n\n// Customer data \ncustomerStore.customer?.email\n```\n\n### Observer (for sub-components with reactive updates)\n```typescript\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span>Cart: {itemCount}</span>;\n});\n\n// Root export: plain function — ikas runtime handles reactivity via autorun()\nexport function MySection({ title }: Props) {\n return (\n <div>\n <h1>{title}</h1>\n <CartBadge />\n </div>\n );\n}\n\nexport default MySection;\n```\n\nDo **NOT** wrap root component exports with `observer()`. The ikas runtime handles root reactivity automatically via `autorun()`. Only wrap **sub-components** with `observer()` when they independently read from stores (`cartStore`, `customerStore`).\n\n### ⛔ Never deep-import from the SDK\n\nImport ONLY from the bare package root `@ikas/bp-storefront` (and likewise `@ikas/bp-storefront-models`/`-config`). **NEVER** import a subpath such as `@ikas/bp-storefront/dist/...` or `@ikas/bp-storefront/src/...`. Those private paths bundle without error but have NO window global in the published storefront, so the component throws at runtime — and `ikas-component build`/`dev` now hard-rejects them. If a helper looks missing from the root, it almost always exists there under a public name. Examples:\n\n- Coupon code → `getCouponCodeForm(customerStore)`, `initCouponCodeForm`, `setCouponCodeFormCouponCode`, `submitCouponCodeForm`, `removeCouponCodeForm`\n- Cart note / generic cart save → `saveCart`\n- Cart data → `cartStore`, `getCart`\n\n### Known misspelled export — import it verbatim\n\n`getIkasProductAttrributeProductIds` is spelled with a double **r** (\"Attrribute\"). That IS the real export name: it ships in `@ikas/bp-storefront` and is kept as-is because renaming a published export would break every theme importing it. Use it exactly as written — the correctly spelled `getIkasProductAttributeProductIds` does not exist and will not compile. Do NOT report it as a typo or \"fix\" the spelling. (In the blueprint editor its display alias is `getProductIds`, which hides the misspelling.)",
161
+ "tags": ["import", "storefront", "models", "types", "observer", "stores"]
154
162
  },
155
163
  "sections-vs-components": {
156
164
  "title": "Sections vs Components",
@@ -173,7 +181,7 @@
173
181
  "common-pitfalls": {
174
182
  "title": "Common Pitfalls",
175
183
  "description": "Frequent mistakes LLMs and developers make when building ikas code components",
176
- "content": "## Common Pitfalls\n\nThese are the most common mistakes when building ikas code components. Avoid them for correct, working code.\n\n### 1. Root Component Should NOT Use observer\n\nThe ikas runtime wraps root component renders in `autorun()`, making them automatically reactive. Wrapping a root export with `observer()` is redundant and misleading.\n\n**Wrong** — observer on root export:\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nconst CartSection = observer(function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n});\nexport default CartSection;\n```\n\n**Correct** — plain named export:\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nexport function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n}\n\nexport default CartSection;\n```\n\n### 2. Observer Sub-Component Naming\n\nWhen using `observer()` on **sub-components**, use a named function expression — not an arrow function — for proper DevTools display names.\n\n**Wrong** — arrow function loses display name:\n```tsx\nconst CartBadge = observer(() => {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n**Correct** — named function expression:\n```tsx\nconst CartBadge = observer(function CartBadge() {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n### 3. Mutation Semantics\n\nMany storefront functions (122+) return `void` and **mutate their arguments in place**. Do NOT try to capture a return value:\n\n```tsx\n// WRONG — selectVariantValue returns void, not a new product\nconst updated = selectVariantValue(product, value);\n\n// CORRECT — mutates product in place, observer re-renders automatically\nselectVariantValue(product, dvv.variantValue);\n```\n\nOther mutation functions: `initLoginForm()`, `setLoginFormEmail()`, `clearFilter()`, `selectFilterValue()`.\n\n### 4. CSS Scoping Limits\n\nOnly **class selectors** in `styles.css` are reliably scoped. Element selectors are NOT scoped:\n\n```css\n/* SAFE — scoped to your component */\n.product-card { padding: 16px; }\n.product-card .title { font-size: 18px; }\n\n/* UNSAFE — NOT reliably scoped, may affect other components */\ndiv { margin: 0; }\nh1 { font-size: 24px; }\n```\n\nAlways use class selectors for all styles.\n\n### 5. Prop Null Handling\n\nProps from the editor can be `undefined` when the store owner hasn't set them. Always use optional chaining:\n\n```tsx\n// WRONG — will crash if product is undefined\n<h1>{props.product.name}</h1>\n\n// CORRECT\n<h1>{props.product?.name}</h1>\n{props.heroImage && <img src={getDefaultSrc(props.heroImage)} />}\n```\n\n### 6. IkasProductImage vs IkasImage\n\n`variant.images` is `IkasProductImage[]`, NOT `IkasImage[]`. You must access the `.image` property to get the `IkasImage` needed by CDN helpers:\n\n```tsx\n// WRONG — productImage is IkasProductImage, not IkasImage\ngetDefaultSrc(productImage);\n\n// CORRECT — access .image to get IkasImage\ngetDefaultSrc(productImage.image);\n\n// Full pattern:\nconst images: IkasImage[] = variant.images\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### 7. Type Assertion Pattern\n\nSome storefront functions have type inference gaps. Use `as unknown as` casts when needed — this is a known pattern:\n\n```tsx\nconst inStock = hasProductStock(product) as unknown as boolean;\nconst finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\nconst canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n```\n\nThis applies to functions like `hasProductStock`, `hasProductVariantStock`, `isAddToCartEnabled`, `hasProductVariantDiscount`, `getProductVariantDiscountPercentage`, `getProductVariantFormattedFinalPrice`, `getProductVariantFormattedSellPrice`, `getProductVariantFormattedDiscountAmount`, and `getProductVariantFormattedCampaignPrice`.\n\nNote: `getProductVariantMainImage` returns `IkasProductImage | undefined` (NOT `IkasImage`). Access `.image` to get the `IkasImage` for CDN helpers like `getDefaultSrc()`.\n\n### 8. Store Data Null Safety\n\nStore data (`customerStore.customer`, `cartStore.cart`, `baseStore`) is `null` before initialization completes. Always guard access:\n\n```tsx\n// WRONG — crashes if customer is null\n<h1>{customerStore.customer.firstName}</h1>\n\n// CORRECT — null check first\nif (!customerStore.customer) return <div>Loading...</div>;\n<h1>{customerStore.customer.firstName}</h1>\n\n// Also correct — optional chaining\n<h1>{customerStore.customer?.firstName ?? \"Guest\"}</h1>\n```\n\nSame for `cartStore.cart` — always use `cartStore.cart?.orderLineItems ?? []`.\n\n### 9. ProductList/BlogList Data Access\n\n`productList.data` is the correct way to access products in a product list. Similarly, `blogList.data` for blogs. The display names `.products` / `.blogs` are only used by the blueprint editor — they do NOT exist at runtime:\n\n```tsx\n// CORRECT — use .data for both product lists and blog lists\nconst products = productList?.data ?? [];\nconst blogs = blogList?.data ?? [];\n\n// WRONG — .products / .blogs are display names, not actual fields\nconst products = productList?.products ?? [];\nconst blogs = blogList?.blogs ?? [];\n```\n\n### 10. Form Field Access Pattern\n\nForm fields are objects with `.value`, `.label`, `.hasError`, `.message`. Never access the field directly as a primitive:\n\n```tsx\n// WRONG — loginForm.email is a field object, not a string\n<input value={loginForm.email} />\n\n// CORRECT — access .value for the actual value\n<input value={loginForm.email.value} />\n{loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n```\n\n### 11. Optional Chaining for Editor Props\n\nAll props from `ikas.config.json` can be `undefined` in the editor before the store owner sets them. Always use optional chaining and defaults:\n\n```tsx\n// WRONG — will crash in the editor\n<h1>{props.title}</h1>\n<img src={getDefaultSrc(props.image)} />\n{props.links.links.map(...)}\n\n// CORRECT — safe access with defaults\n<h1>{props.title ?? \"Default Title\"}</h1>\n{props.image && <img src={getDefaultSrc(props.image)} />}\n{(props.links?.links ?? []).map(...)}\n```\n\n### 12. Event Handler Typing\n\nPreact uses different event types than React. Use `(e: Event)` not `(e: React.ChangeEvent)`. Access values with casting. Preact uses `onInput` not `onChange` for text inputs:\n\n```tsx\n// WRONG — React patterns don't work in Preact\nonChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n\n// CORRECT — Preact event handling\nonInput={(e: Event) => setValue((e.target as HTMLInputElement).value)}\n```\n\nFor select elements:\n```tsx\nonChange={(e: Event) => setOption((e.target as HTMLSelectElement).value)}\n```\n\n### 13. Function Parameter Order\n\nMany storefront functions take specific parameter orders. Always verify with `get_function_doc()` before using:\n\n```tsx\n// WRONG — submitLoginForm only takes the form, not the store\nsubmitLoginForm(customerStore, loginForm);\n\n// CORRECT\nsubmitLoginForm(loginForm);\n\n// WRONG — wrong parameter order for addItemToCart\naddItemToCart(product, variant, 1);\n\n// CORRECT — variant first, then product, then quantity\naddItemToCart(variant, product, 1);\n\n// WRONG — selectVariantValue takes product and variantValue\nselectVariantValue(variant, value);\n\n// CORRECT\nselectVariantValue(product, dvv.variantValue);\n```\n\nWhen in doubt, use the `get_function_doc(functionName)` MCP tool to check the exact signature.\n\n### 14. Missing backgroundColor Prop on Sections\n\nEvery section MUST include a `backgroundColor` COLOR prop so store owners can customize the section background. Without it, the section has a hardcoded background that cannot be changed in the editor.\n\n**Wrong** — section with no backgroundColor prop:\n```json\n// ikas.config.json\n{ \"id\": \"my-section\", \"type\": \"section\", \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\" }\n]}\n```\n```tsx\nexport function MySection({ title }: Props) {\n return <section className=\"my-section\"><h1>{title}</h1></section>;\n}\n```\n\n**Correct** — section with backgroundColor prop:\n```json\n// ikas.config.json\n{ \"id\": \"my-section\", \"type\": \"section\", \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\" },\n { \"name\": \"backgroundColor\", \"displayName\": \"Background Color\", \"type\": \"COLOR\", \"defaultValue\": \"#ffffff\" }\n]}\n```\n```tsx\nexport function MySection({ title, backgroundColor = \"#ffffff\" }: Props) {\n return (\n <section className=\"my-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>{title}</h1>\n </section>\n );\n}\n```\n\nOptionally, also consider adding `textColor` COLOR props for text elements sitting directly on the section background, so they remain readable when the store owner changes the background color.\n\n### 15. Forgetting to Group Props for Complex Sections\n\nSections with 5+ props should use prop groups to organize the editor sidebar. Without groups, store owners see a long flat list of unrelated props.\n\n**Better** — organize related props into groups:\n```json\n{\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\" },\n { \"id\": \"appearance\", \"name\": \"Appearance\" }\n ],\n \"props\": [\n { \"name\": \"heading\", \"groupId\": \"content\", ... },\n { \"name\": \"bgColor\", \"groupId\": \"appearance\", ... }\n ]\n}\n```\n\nSee `get_framework_guide(\"prop-groups\")` for full details.\n\n### 16. Hardcoded Static Text in Components\n\nAll user-visible text MUST be TEXT props — never hardcode strings in JSX. Hardcoded text cannot be translated for multilingual storefronts.\n\n**Wrong** — hardcoded strings:\n```tsx\nexport function LoginSection({ backgroundColor }: Props) {\n return (\n <section style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>Sign In</h1>\n <button>{form.isSubmitting ? \"Signing in...\" : \"Sign In\"}</button>\n <p>Don't have an account? <a href=\"/register\">Create one</a></p>\n </section>\n );\n}\n```\n\n**Correct** — all text as TEXT props with defaultValues:\n```json\n// ikas.config.json\n{ \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\", \"defaultValue\": \"Sign In\", \"groupId\": \"texts\" },\n { \"name\": \"submitButtonText\", \"type\": \"TEXT\", \"defaultValue\": \"Sign In\", \"groupId\": \"texts\" },\n { \"name\": \"submittingButtonText\", \"type\": \"TEXT\", \"defaultValue\": \"Signing in...\", \"groupId\": \"texts\" },\n { \"name\": \"noAccountText\", \"type\": \"TEXT\", \"defaultValue\": \"Don't have an account?\", \"groupId\": \"texts\" },\n { \"name\": \"createAccountLinkText\", \"type\": \"TEXT\", \"defaultValue\": \"Create one\", \"groupId\": \"texts\" }\n],\n\"propGroups\": [{ \"id\": \"texts\", \"name\": \"Texts\" }]}\n```\n```tsx\nexport function LoginSection({ title = \"Sign In\", submitButtonText = \"Sign In\",\n submittingButtonText = \"Signing in...\", noAccountText = \"Don't have an account?\",\n createAccountLinkText = \"Create one\", backgroundColor }: Props) {\n return (\n <section style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>{title}</h1>\n <button>{form.isSubmitting ? submittingButtonText : submitButtonText}</button>\n <p>{noAccountText} <a href=\"/register\">{createAccountLinkText}</a></p>\n </section>\n );\n}\n```\n\nFor button loading states, use two separate TEXT props (e.g., `submitButtonText` + `submittingButtonText`). Group text props under a \"Texts\" propGroup when the component has 5+ props.\n\n### 17. IkasOrderLineItemOption.values is an Array, Not a Scalar\n\n`IkasOrderLineItemOption` (the per-item options on cart/order line items) has a `.values` field that is an **array** of `IkasOrderLineItemOptionValue`, NOT a scalar `.value`. Each value in the array has its own `.name` / `.value` / `.formattedPrice`.\n\n```tsx\n// WRONG — there is no .value on IkasOrderLineItemOption\n<span>{option.value}</span>\n\n// CORRECT — .values is an array; map over it\n{option.values.map((v) => (\n <span key={v.name}>{v.name}: {v.value}</span>\n))}\n```\n\nThis is the most common confusion when rendering order summary lines or cart line item options. See `get_migration_guide(\"prop-runtime-shapes\")` for the broader catalog of order-shape gotchas.\n\n### 18. IkasOrderAdjustment Has No .formattedAmount — Use the Helper\n\n`IkasOrderAdjustment` does not expose a `.formattedAmount` property directly. Call the helper `getOrderAdjustmentFormattedAmount(adjustment)` from `@ikas/bp-storefront` — it returns the formatted currency string with the correct sign (negated for decrements).\n\n```tsx\nimport { getOrderAdjustmentFormattedAmount } from \"@ikas/bp-storefront\";\n\n// WRONG — .formattedAmount does not exist\n<td>{adjustment.formattedAmount}</td>\n\n// CORRECT\n<td>{getOrderAdjustmentFormattedAmount(adjustment)}</td>\n```\n\nSee `get_function_doc(\"getOrderAdjustmentFormattedAmount\")` for the exact signature.\n\n### 19. Old-System Property Patterns That Don't Exist in the New System\n\nSeveral `@ikas/storefront` properties look like they should still work but don't exist on the corresponding `@ikas/bp-storefront` types. Use the helper function instead. **If TypeScript reports TS2339 (\"Property X does not exist on type Y\") on a `.selectedVariant`, `.mainImage`, `.href`, or `.checkoutUrl` access, you're hitting one of these.**\n\n| Old pattern | New replacement |\n|---|---|\n| `product.selectedVariant` | `getSelectedProductVariant(product)` → `IkasProductVariant \\| undefined` |\n| `product.href` | `getProductHref(product)` (one of several `get*Href` helpers — see `navigation-patterns`) |\n| `variant.mainImage` | `getProductVariantMainImage(variant)` → `IkasProductImage`, then access `.image` for the `IkasImage` |\n| `cartStore.checkoutUrl` | `getCheckoutUrlFromCartStore(cartStore)` (handles auth-state branching internally) |\n\nThe variant-to-image chain is 4 hops (each carries metadata the next doesn't). See `get_framework_guide(\"image-handling\")` for the canonical recipe.",
184
+ "content": "## Common Pitfalls\n\nThese are the most common mistakes when building ikas code components. Avoid them for correct, working code.\n\n### 1. Root Component Should NOT Use observer\n\nThe ikas runtime wraps root component renders in `autorun()`, making them automatically reactive. Wrapping a root export with `observer()` is redundant and misleading.\n\n**Wrong** — observer on root export:\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nconst CartSection = observer(function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n});\nexport default CartSection;\n```\n\n**Correct** — plain named export:\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nexport function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n}\n\nexport default CartSection;\n```\n\n### 2. Observer Sub-Component Naming\n\nWhen using `observer()` on **sub-components**, use a named function expression — not an arrow function — for proper DevTools display names.\n\n**Wrong** — arrow function loses display name:\n```tsx\nconst CartBadge = observer(() => {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n**Correct** — named function expression:\n```tsx\nconst CartBadge = observer(function CartBadge() {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n### 3. Mutation Semantics\n\nMany storefront functions (122+) return `void` and **mutate their arguments in place**. Do NOT try to capture a return value:\n\n```tsx\n// WRONG — selectVariantValue returns void, not a new product\nconst updated = selectVariantValue(product, value);\n\n// CORRECT — mutates product in place, observer re-renders automatically\nselectVariantValue(product, dvv.variantValue);\n```\n\nOther mutation functions: `initLoginForm()`, `setLoginFormEmail()`, `clearFilter()`, `selectFilterValue()`.\n\n### 4. CSS Scoping Limits\n\nOnly **class selectors** in `styles.css` are reliably scoped. Element selectors are NOT scoped:\n\n```css\n/* SAFE — scoped to your component */\n.product-card { padding: 16px; }\n.product-card .title { font-size: 18px; }\n\n/* UNSAFE — NOT reliably scoped, may affect other components */\ndiv { margin: 0; }\nh1 { font-size: 24px; }\n```\n\nAlways use class selectors for all styles.\n\n### 5. Prop Null Handling\n\nProps from the editor can be `undefined` when the store owner hasn't set them. Always use optional chaining:\n\n```tsx\n// WRONG — will crash if product is undefined\n<h1>{props.product.name}</h1>\n\n// CORRECT\n<h1>{props.product?.name}</h1>\n{props.heroImage && <img src={getDefaultSrc(props.heroImage)} />}\n```\n\n### 6. IkasProductImage vs IkasImage\n\n`variant.images` is `IkasProductImage[]`, NOT `IkasImage[]`. You must access the `.image` property to get the `IkasImage` needed by CDN helpers:\n\n```tsx\n// WRONG — productImage is IkasProductImage, not IkasImage\ngetDefaultSrc(productImage);\n\n// CORRECT — access .image to get IkasImage\ngetDefaultSrc(productImage.image);\n\n// Full pattern:\nconst images: IkasImage[] = variant.images\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### 7. Type Assertion Pattern\n\nSome storefront functions have type inference gaps. Use `as unknown as` casts when needed — this is a known pattern:\n\n```tsx\nconst inStock = hasProductStock(product) as unknown as boolean;\nconst finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\nconst canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n```\n\nThis applies to functions like `hasProductStock`, `hasProductVariantStock`, `isAddToCartEnabled`, `hasProductVariantDiscount`, `getProductVariantDiscountPercentage`, `getProductVariantFormattedFinalPrice`, `getProductVariantFormattedSellPrice`, `getProductVariantFormattedDiscountAmount`, and `getProductVariantFormattedCampaignPrice`.\n\nNote: `getProductVariantMainImage` returns `IkasProductImage | undefined` (NOT `IkasImage`). Access `.image` to get the `IkasImage` for CDN helpers like `getDefaultSrc()`.\n\n### 8. Store Data Null Safety\n\nStore data (`customerStore.customer`, `cartStore.cart`, `baseStore`) is `null` before initialization completes. Always guard access:\n\n```tsx\n// WRONG — crashes if customer is null\n<h1>{customerStore.customer.firstName}</h1>\n\n// CORRECT — null check first\nif (!customerStore.customer) return <div>Loading...</div>;\n<h1>{customerStore.customer.firstName}</h1>\n\n// Also correct — optional chaining\n<h1>{customerStore.customer?.firstName ?? \"Guest\"}</h1>\n```\n\nSame for `cartStore.cart` — always use `cartStore.cart?.orderLineItems ?? []`.\n\n### 9. ProductList/BlogList Data Access\n\n`productList.data` is the correct way to access products in a product list. Similarly, `blogList.data` for blogs. The display names `.products` / `.blogs` are only used by the blueprint editor — they do NOT exist at runtime:\n\n```tsx\n// CORRECT — use .data for both product lists and blog lists\nconst products = productList?.data ?? [];\nconst blogs = blogList?.data ?? [];\n\n// WRONG — .products / .blogs are display names, not actual fields\nconst products = productList?.products ?? [];\nconst blogs = blogList?.blogs ?? [];\n```\n\n### 10. Form Field Access Pattern\n\nForm fields are objects with `.value`, `.label`, `.hasError`, `.message`. Never access the field directly as a primitive:\n\n```tsx\n// WRONG — loginForm.email is a field object, not a string\n<input value={loginForm.email} />\n\n// CORRECT — access .value for the actual value\n<input value={loginForm.email.value} />\n{loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n```\n\n### 11. Optional Chaining for Editor Props\n\nAll props from `ikas.config.json` can be `undefined` in the editor before the store owner sets them. Always use optional chaining and defaults:\n\n```tsx\n// WRONG — will crash in the editor\n<h1>{props.title}</h1>\n<img src={getDefaultSrc(props.image)} />\n{props.links.links.map(...)}\n\n// CORRECT — safe access with defaults\n<h1>{props.title ?? \"Default Title\"}</h1>\n{props.image && <img src={getDefaultSrc(props.image)} />}\n{(props.links?.links ?? []).map(...)}\n```\n\n### 12. Event Handler Typing\n\nPreact uses different event types than React. Use `(e: Event)` not `(e: React.ChangeEvent)`. Access values with casting. Preact uses `onInput` not `onChange` for text inputs:\n\n```tsx\n// WRONG — React patterns don't work in Preact\nonChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n\n// CORRECT — Preact event handling\nonInput={(e: Event) => setValue((e.target as HTMLInputElement).value)}\n```\n\nFor select elements:\n```tsx\nonChange={(e: Event) => setOption((e.target as HTMLSelectElement).value)}\n```\n\n### 13. Function Parameter Order\n\nMany storefront functions take specific parameter orders. Always verify with `get_function_doc()` before using:\n\n```tsx\n// WRONG — submitLoginForm only takes the form, not the store\nsubmitLoginForm(customerStore, loginForm);\n\n// CORRECT\nsubmitLoginForm(loginForm);\n\n// WRONG — wrong parameter order for addItemToCart\naddItemToCart(product, variant, 1);\n\n// CORRECT — variant first, then product, then quantity\naddItemToCart(variant, product, 1);\n\n// WRONG — selectVariantValue takes product and variantValue\nselectVariantValue(variant, value);\n\n// CORRECT\nselectVariantValue(product, dvv.variantValue);\n```\n\nWhen in doubt, use the `get_function_doc(functionName)` MCP tool to check the exact signature.\n\n### 14. Missing backgroundColor Prop on Sections\n\nEvery section MUST include a `backgroundColor` COLOR prop so store owners can customize the section background. Without it, the section has a hardcoded background that cannot be changed in the editor.\n\n**Wrong** — section with no backgroundColor prop:\n```json\n// ikas.config.json\n{ \"id\": \"my-section\", \"type\": \"section\", \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\" }\n]}\n```\n```tsx\nexport function MySection({ title }: Props) {\n return <section className=\"my-section\"><h1>{title}</h1></section>;\n}\n```\n\n**Correct** — section with backgroundColor prop:\n```json\n// ikas.config.json\n{ \"id\": \"my-section\", \"type\": \"section\", \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\" },\n { \"name\": \"backgroundColor\", \"displayName\": \"Background Color\", \"type\": \"COLOR\", \"defaultValue\": \"#ffffff\" }\n]}\n```\n```tsx\nexport function MySection({ title, backgroundColor = \"#ffffff\" }: Props) {\n return (\n <section className=\"my-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>{title}</h1>\n </section>\n );\n}\n```\n\nOptionally, also consider adding `textColor` COLOR props for text elements sitting directly on the section background, so they remain readable when the store owner changes the background color.\n\n### 15. Forgetting to Group Props for Complex Sections\n\nSections with 5+ props should use prop groups to organize the editor sidebar. Without groups, store owners see a long flat list of unrelated props.\n\n**Better** — organize related props into groups:\n```json\n{\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\" },\n { \"id\": \"appearance\", \"name\": \"Appearance\" }\n ],\n \"props\": [\n { \"name\": \"heading\", \"groupId\": \"content\", ... },\n { \"name\": \"bgColor\", \"groupId\": \"appearance\", ... }\n ]\n}\n```\n\nSee `get_framework_guide(\"prop-groups\")` for full details.\n\n### 16. Hardcoded Static Text in Components\n\nAll user-visible text MUST be TEXT props — never hardcode strings in JSX. Hardcoded text cannot be translated for multilingual storefronts.\n\n**Wrong** — hardcoded strings:\n```tsx\nexport function LoginSection({ backgroundColor }: Props) {\n return (\n <section style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>Sign In</h1>\n <button>{form.isSubmitting ? \"Signing in...\" : \"Sign In\"}</button>\n <p>Don't have an account? <a href=\"/register\">Create one</a></p>\n </section>\n );\n}\n```\n\n**Correct** — all text as TEXT props with defaultValues:\n```json\n// ikas.config.json\n{ \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\", \"defaultValue\": \"Sign In\", \"groupId\": \"texts\" },\n { \"name\": \"submitButtonText\", \"type\": \"TEXT\", \"defaultValue\": \"Sign In\", \"groupId\": \"texts\" },\n { \"name\": \"submittingButtonText\", \"type\": \"TEXT\", \"defaultValue\": \"Signing in...\", \"groupId\": \"texts\" },\n { \"name\": \"noAccountText\", \"type\": \"TEXT\", \"defaultValue\": \"Don't have an account?\", \"groupId\": \"texts\" },\n { \"name\": \"createAccountLinkText\", \"type\": \"TEXT\", \"defaultValue\": \"Create one\", \"groupId\": \"texts\" }\n],\n\"propGroups\": [{ \"id\": \"texts\", \"name\": \"Texts\" }]}\n```\n```tsx\nexport function LoginSection({ title = \"Sign In\", submitButtonText = \"Sign In\",\n submittingButtonText = \"Signing in...\", noAccountText = \"Don't have an account?\",\n createAccountLinkText = \"Create one\", backgroundColor }: Props) {\n return (\n <section style={backgroundColor ? { backgroundColor } : undefined}>\n <h1>{title}</h1>\n <button>{form.isSubmitting ? submittingButtonText : submitButtonText}</button>\n <p>{noAccountText} <a href=\"/register\">{createAccountLinkText}</a></p>\n </section>\n );\n}\n```\n\nFor button loading states, use two separate TEXT props (e.g., `submitButtonText` + `submittingButtonText`). Group text props under a \"Texts\" propGroup when the component has 5+ props.\n\n### 17. IkasOrderLineItemOption.values is an Array, Not a Scalar\n\n`IkasOrderLineItemOption` (the per-item options on cart/order line items) has a `.values` field that is an **array** of `IkasOrderLineItemOptionValue`, NOT a scalar `.value`. Each value in the array has its own `.name` / `.value` / `.formattedPrice`.\n\n```tsx\n// WRONG — there is no .value on IkasOrderLineItemOption\n<span>{option.value}</span>\n\n// CORRECT — .values is an array; map over it\n{option.values.map((v) => (\n <span key={v.name}>{v.name}: {v.value}</span>\n))}\n```\n\nThis is the most common confusion when rendering order summary lines or cart line item options. See `get_migration_guide(\"prop-runtime-shapes\")` for the broader catalog of order-shape gotchas.\n\n### 18. IkasOrderAdjustment Has No .formattedAmount — Use the Helper\n\n`IkasOrderAdjustment` does not expose a `.formattedAmount` property directly. Call the helper `getOrderAdjustmentFormattedAmount(adjustment)` from `@ikas/bp-storefront` — it returns the formatted currency string with the correct sign (negated for decrements).\n\n```tsx\nimport { getOrderAdjustmentFormattedAmount } from \"@ikas/bp-storefront\";\n\n// WRONG — .formattedAmount does not exist\n<td>{adjustment.formattedAmount}</td>\n\n// CORRECT\n<td>{getOrderAdjustmentFormattedAmount(adjustment)}</td>\n```\n\nSee `get_function_doc(\"getOrderAdjustmentFormattedAmount\")` for the exact signature.\n\n### 19. Old-System Property Patterns That Don't Exist in the New System\n\nSeveral `@ikas/storefront` properties look like they should still work but don't exist on the corresponding `@ikas/bp-storefront` types. Use the helper function instead. **If TypeScript reports TS2339 (\"Property X does not exist on type Y\") on a `.selectedVariant`, `.mainImage`, `.href`, or `.checkoutUrl` access, you're hitting one of these.**\n\n| Old pattern | New replacement |\n|---|---|\n| `product.selectedVariant` | `getSelectedProductVariant(product)` → `IkasProductVariant \\| undefined` |\n| `product.href` | `getProductHref(product)` (one of several `get*Href` helpers — see `navigation-patterns`) |\n| `variant.mainImage` | `getProductVariantMainImage(variant)` → `IkasProductImage`, then access `.image` for the `IkasImage` |\n| `cartStore.checkoutUrl` | `getCheckoutUrlFromCartStore(cartStore)` (handles auth-state branching internally) |\n\nThe variant-to-image chain is 4 hops (each carries metadata the next doesn't). See `get_framework_guide(\"image-handling\")` for the canonical recipe.\n\n### 20. Intl.NumberFormat for Currency Amounts\n\nNever format money with `Intl.NumberFormat` — it falls back to the browser locale and ignores the merchant's currency format settings, so its output clashes with platform-formatted prices on the same screen. For model-bound prices use the `get*FormattedPrice` helpers. For free-standing amounts (remaining amount to free shipping, gift wrap threshold, bulk discount limits, cart limit warnings) use `formatCurrency(amount, currencyCode, currencySymbol)` from `@ikas/bp-storefront`.\n\nNote: `Intl.DateTimeFormat(IkasStorefrontConfig.getCurrentLocale(), ...)` IS the correct pattern for dates — do not mirror it for money.\n\n```tsx\n// WRONG — browser-locale format (\"₺14.478,5\") next to platform prices (\"₺ 1,950.00\")\nconst remaining = new Intl.NumberFormat(undefined, { style: \"currency\", currency: \"TRY\" }).format(amount);\n\n// CORRECT — merchant currency format settings\nimport { formatCurrency } from \"@ikas/bp-storefront\";\n\nconst remaining = formatCurrency(amount, cart.currencyCode, cart.currencySymbol);\n```\n\nSee `get_function_doc(\"formatCurrency\")` for the exact signature.\n\n### Deep SDK imports (`@ikas/bp-storefront/dist/...`)\n\n**Wrong** — reaching into a private subpath when a root helper seems missing:\n```tsx\n// breaks at runtime: no window global for this subpath, and the build rejects it\nimport { apiSaveCart } from \"@ikas/bp-storefront/dist/functions/api/cart\";\n```\n\n**Correct** — use the public root export (coupon has dedicated form helpers):\n```tsx\nimport {\n customerStore,\n getCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n} from \"@ikas/bp-storefront\";\n```\n\nRule: every import from the storefront SDK must come from the BARE package name — never a `/dist/` or `/src/` subpath.",
177
185
  "tags": [
178
186
  "pitfalls",
179
187
  "gotchas",
@@ -188,13 +196,17 @@
188
196
  "parameters",
189
197
  "static-text",
190
198
  "translation",
191
- "i18n"
199
+ "i18n",
200
+ "currency",
201
+ "formatCurrency",
202
+ "NumberFormat",
203
+ "price-format"
192
204
  ]
193
205
  },
194
206
  "sub-component-patterns": {
195
207
  "title": "Sub-Component File Organization",
196
208
  "description": "How to organize sub-components in their own folders under src/sub-components/ with proper imports and CSS",
197
- "content": "## Sub-Component File Organization\n\n**ALWAYS create sub-components in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. NEVER create flat .tsx files inside a component folder.**\n\n### Directory Structure\n\n```\nsrc/\n├── components/\n│ ├── ProductList/\n│ │ ├── index.tsx # imports from ../../sub-components/...\n│ │ ├── types.ts # auto-generated (CLI-managed)\n│ │ └── styles.css # @import \"../../sub-components/.../styles.css\"\n│ └── index.ts\n└── sub-components/\n ├── ProductCard/\n │ ├── index.tsx # sub-component\n │ └── styles.css # sub-component styles\n └── FilterSidebar/\n ├── index.tsx # sub-component\n └── styles.css # sub-component styles\n```\n\n### Key Rules\n\n1. **`src/components/`** = registered components (listed in `ikas.config.json`)\n2. **`src/sub-components/`** = internal helpers (NOT in `ikas.config.json`)\n3. Sub-components do **NOT** have `types.ts` — define `Props` interface inline in `index.tsx`\n4. Sub-components that read MobX stores need `observer()` from `@ikas/component-utils`\n5. Sub-components that only receive props do NOT need `observer()`\n6. CSS is scoped with the parent component's `.cc_` prefix (same scope — no conflicts)\n7. Sub-components can be shared across multiple parent sections\n\n### CSS Import Pattern\n\nIn the parent component's `styles.css`, import sub-component styles with `@import`:\n\n```css\n/* src/components/ProductList/styles.css */\n@import \"../../sub-components/ProductCard/styles.css\";\n@import \"../../sub-components/FilterSidebar/styles.css\";\n\n.product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n```\n\nThe build system's CSS import resolver recursively resolves `@import` statements with relative paths. All imported CSS gets scoped with the parent component's `.cc_{componentId}` prefix.\n\n### TSX Import Pattern\n\n```tsx\n// src/components/ProductList/index.tsx\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\nexport function ProductListSection({ productList, title }: Props) {\n const products = productList?.data ?? [];\n return (\n <section className=\"product-list-section\">\n <FilterSidebar productList={productList} />\n <div className=\"product-grid\">\n {products.map((product) => (\n <ProductCard key={product.id} product={product} />\n ))}\n </div>\n </section>\n );\n}\n\nexport default ProductListSection;\n```\n\n### Sub-Component Example\n\n```tsx\n// src/sub-components/ProductCard/index.tsx\nimport {\n IkasProduct,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\ninterface Props {\n product: IkasProduct;\n}\n\nexport default function ProductCard({ product }: Props) {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n\n return (\n <a href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n}\n```\n\nNotice:\n- `Props` is defined inline — no separate `types.ts` file\n- No `observer()` needed because this component only uses props passed from the parent\n- Default export like registered components\n\n### When to Extract Sub-Components\n\n- A section's `index.tsx` exceeds ~150 lines\n- A distinct UI block (card, sidebar, modal) has its own styles and logic\n- The same UI block is used by multiple parent sections\n- A part of the component independently reads MobX stores (extract + wrap with `observer()`)\n\n### When observer() Is Needed on Sub-Components\n\n```tsx\n// src/sub-components/CartBadge/index.tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// This sub-component independently reads a MobX store → needs observer()\nconst CartBadge = observer(function CartBadge() {\n const count = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{count}</span>;\n});\n\nexport default CartBadge;\n```",
209
+ "content": "## Sub-Component File Organization\n\n**ALWAYS create sub-components in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. NEVER create flat .tsx files inside a component folder.**\n\n### Directory Structure\n\n```\nsrc/\n├── components/\n│ ├── ProductList/\n│ │ ├── index.tsx # imports from ../../sub-components/...\n│ │ ├── types.ts # auto-generated (CLI-managed)\n│ │ └── styles.css # component styles (sub-component styles.css collected automatically)\n│ └── index.ts\n└── sub-components/\n ├── ProductCard/\n │ ├── index.tsx # sub-component\n │ └── styles.css # sub-component styles\n └── FilterSidebar/\n ├── index.tsx # sub-component\n └── styles.css # sub-component styles\n```\n\n### Key Rules\n\n1. **`src/components/`** = registered components (listed in `ikas.config.json`)\n2. **`src/sub-components/`** = internal helpers (NOT in `ikas.config.json`)\n3. Sub-components do **NOT** have `types.ts` — define `Props` interface inline in `index.tsx`\n4. Sub-components that read MobX stores need `observer()` from `@ikas/component-utils`\n5. Sub-components that only receive props do NOT need `observer()`\n6. CSS is scoped with the parent component's `.cc_` prefix (same scope — no conflicts)\n7. Sub-components can be shared across multiple parent sections\n\n### Sub-Component CSS (collected automatically)\n\nA sub-component's sibling `styles.css` is picked up WITHOUT any import: `dev` and `publish` walk each component's import graph and collect every `styles.css` sitting next to a source file it uses. CSS of a sub-component used by ONE component is inlined into that component's CSS; CSS of a SHARED sub-component is emitted once as shared chunk CSS, scoped for every consumer. Either way it ends up under the component's `.cc_{componentId}` prefix. The section templates rely on this — none of them write `@import`.\n\n```css\n/* src/components/ProductList/styles.css — no @import needed */\n.product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n```\n\n`@import \"../../sub-components/ProductCard/styles.css\";` in the parent's `styles.css` still works (the resolver inlines relative imports recursively) and is only REQUIRED for the standalone `build` command, which compiles one component at a time and sees nothing but the config's `styles` file and its `@import`s. The compiler skips an `@import` whose file is already covered by the component's shared chunk CSS, so writing it never duplicates rules. Never `import \"./styles.css\"` from TSX — CSS only ever flows through the two paths above.\n\n### TSX Import Pattern\n\n```tsx\n// src/components/ProductList/index.tsx\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\nexport function ProductListSection({ productList, title }: Props) {\n const products = productList?.data ?? [];\n return (\n <section className=\"product-list-section\">\n <FilterSidebar productList={productList} />\n <div className=\"product-grid\">\n {products.map((product) => (\n <ProductCard key={product.id} product={product} />\n ))}\n </div>\n </section>\n );\n}\n\nexport default ProductListSection;\n```\n\n### Sub-Component Example\n\n```tsx\n// src/sub-components/ProductCard/index.tsx\nimport {\n IkasProduct,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\ninterface Props {\n product: IkasProduct;\n}\n\nexport default function ProductCard({ product }: Props) {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n\n return (\n <a href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n}\n```\n\nNotice:\n- `Props` is defined inline — no separate `types.ts` file\n- No `observer()` needed because this component only uses props passed from the parent\n- Default export like registered components\n\n### When to Extract Sub-Components\n\n- A section's `index.tsx` exceeds ~150 lines\n- A distinct UI block (card, sidebar, modal) has its own styles and logic\n- The same UI block is used by multiple parent sections\n- A part of the component independently reads MobX stores (extract + wrap with `observer()`)\n\n### When observer() Is Needed on Sub-Components\n\n```tsx\n// src/sub-components/CartBadge/index.tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// This sub-component independently reads a MobX store → needs observer()\nconst CartBadge = observer(function CartBadge() {\n const count = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{count}</span>;\n});\n\nexport default CartBadge;\n```",
198
210
  "tags": [
199
211
  "sub-component",
200
212
  "folder",
@@ -203,13 +215,16 @@
203
215
  "extract",
204
216
  "refactor",
205
217
  "css",
206
- "import"
218
+ "import",
219
+ "chunk",
220
+ "shared",
221
+ "styles.css"
207
222
  ]
208
223
  },
209
224
  "ai-workflow": {
210
225
  "title": "AI Agent Workflow for Building ikas Components",
211
226
  "description": "Step-by-step guide for AI coding agents building ikas storefront components using CLI commands and MCP tools",
212
- "content": "## AI Agent Workflow\n\nThis is the recommended step-by-step workflow for AI agents building ikas code components. Follow these steps in order for reliable, error-free results.\n\n**IMPORTANT: NEVER create or edit `types.ts` manually — it is auto-generated by the CLI.** The CLI commands below update BOTH `ikas.config.json` AND `types.ts` automatically.\n\n### Step 1: Create Component with Props\n\nUse `get_section_template(sectionType)` to get a starter template — it includes a ready-to-run CLI command with `--props`.\n\n**Important:** When creating a section, always include a `backgroundColor` COLOR prop (default: `#ffffff`). Optionally consider adding `textColor` for text elements directly on the section background.\n\n**Important:** When creating a header or footer section, use `--isHeader` or `--isFooter` flags on the `add-component` command. This marks the section as the store's common header/footer so it appears on all pages automatically:\n```bash\nnpx ikas-component config add-component --name \"Header\" --type section --isHeader --props '[...]'\nnpx ikas-component config add-component --name \"Footer\" --type section --isFooter --props '[...]'\n```\n\n**Important:** Ensure ALL user-visible text (headings, buttons, labels, empty states, loading text) is exposed as TEXT props. Never hardcode text strings in JSX. Use `defaultValue` for English defaults. For button loading states, use two separate props (e.g., `submitButtonText` + `submittingButtonText`). Group text props under a \"Texts\" propGroup when 5+ props exist.\n\nThen run the single `add-component --props` command to create the component scaffold WITH all props in one step:\n```bash\nnpx ikas-component config add-component --name \"HeroSection\" --type section --props '[{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\",\"required\":true},{\"name\":\"backgroundImage\",\"displayName\":\"Background Image\",\"type\":\"IMAGE\"},{\"name\":\"showButton\",\"displayName\":\"Show Button\",\"type\":\"BOOLEAN\"}]'\n```\n\nThis creates the component directory with `index.tsx`, `types.ts` (with correct Props interface), `styles.css`, updates `ikas.config.json`, and updates the barrel export. The output is JSON:\n```json\n{\"success\": true, \"componentId\": \"abc123-hero-section\", \"componentName\": \"HeroSection\", \"type\": \"section\", \"propsCount\": 3, \"files\": [...]}\n```\n\nThe `--props` flag accepts a JSON array of prop objects. Each prop needs `name` + `type` at minimum. `displayName` is auto-generated from camelCase name if omitted (e.g. `backgroundImage` → `\"Background Image\"`).\n\nTo add more props later, use `add-prop`:\n```bash\nnpx ikas-component config add-prop --component \"HeroSection\" --name \"subtitle\" --displayName \"Subtitle\" --type TEXT\n```\n\n### Step 2: Get Reference Material\n\nBefore writing component code, use MCP tools to get the right patterns:\n- `get_section_template(sectionType)` — Get the API integration pattern reference for common section types (product-detail, cart, login, header, footer, etc.). Study the function calls and data access patterns only.\n- `get_framework_guide(\"common-pitfalls\")` — Review common mistakes to avoid\n- `get_framework_guide(\"component-structure\")` — Review component structure patterns\n- `get_function_doc(functionName)` — Look up exact function signatures before using them\n- `get_model_guide(typeName)` — Get comprehensive info about a model type\n\n### Step 2.5: Design Your Component (IMPORTANT)\n\nBefore writing code, plan an ORIGINAL visual design:\n- Do NOT reproduce layout, CSS class names, or HTML structure from templates/examples\n- Templates and examples demonstrate correct API usage only — they are not designs to copy\n- **KEEP from references:** imports, function calls, data access patterns, form handling flows, type casts, null safety patterns\n- **CREATE fresh:** all JSX/HTML structure, all CSS class names and rules, all visual layout and spacing, all UX patterns (loading states, empty states, transitions, responsive breakpoints)\n- Think about the specific store's brand and design language when creating your component\n\n### Step 3: Write the Component Code\n\nEdit `src/components/{ComponentName}/index.tsx` with the implementation. **Do NOT edit `types.ts`** — it was already generated correctly in Step 1. Key rules:\n- Import props from `./types` (auto-generated in Step 1)\n- Import storefront functions from `@ikas/bp-storefront`\n- Root export should use named + default export: `export function X({ ... }: Props) { ... }` + `export default X;`\n- Only wrap sub-components with `observer()` when they independently read MobX stores\n- Use optional chaining for all props: `props.title ?? \"Default\"`\n- Use `as unknown as boolean` cast for functions like `hasProductStock`, `isAddToCartEnabled`\n- **Sections MUST apply backgroundColor**: destructure `backgroundColor = \"#ffffff\"` and add `style={backgroundColor ? { backgroundColor } : undefined}` on the root `<section>` element\n\n### Step 3.5: Organize Props into Groups (if 5+ props)\n\nIf the component has 5 or more props, organize them into groups for better editor UX:\n```bash\nnpx ikas-component config add-prop-group --component \"HeroSection\" --id content --name \"Content\"\nnpx ikas-component config add-prop-group --component \"HeroSection\" --id appearance --name \"Appearance\"\nnpx ikas-component config update-prop --component \"HeroSection\" --prop title --group content\nnpx ikas-component config update-prop --component \"HeroSection\" --prop backgroundImage --group appearance\n```\n\n### Step 4: Write Styles\n\nEdit `src/components/{ComponentName}/styles.css`. Key rules:\n- Use class selectors only (`.my-class`), never bare element selectors\n- CSS is auto-scoped at build time — no manual namespacing needed\n- For sections: `width: 100%; padding: 64px 24px;` with inner max-width container\n\n### Step 5: Verify with Type Checking\n\nRun the lightweight type checker:\n```bash\nnpx ikas-component check --json\n```\n\nSuccess output:\n```json\n{\"success\": true, \"errors\": []}\n```\n\nError output:\n```json\n{\"success\": false, \"errorCount\": 2, \"errors\": [{\"file\": \"src/components/Hero/index.tsx\", \"line\": 15, \"column\": 3, \"code\": \"TS2339\", \"message\": \"Property 'title' does not exist on type 'Props'\"}]}\n```\n\n### Step 6: Fix Errors and Re-check\n\nFor each error:\n1. Read the file and line number from the error\n2. Fix the issue (common fixes below)\n3. Re-run `npx ikas-component check --json`\n\n**Common error fixes:**\n- `Property 'x' does not exist on type 'Props'` — Prop wasn't added via CLI. Run `npx ikas-component config add-prop`.\n- `Cannot find module '@ikas/bp-storefront'` — Normal in type-check if node_modules not fully installed. Focus on component-level errors.\n- `Type 'X' is not assignable to type 'Y'` — Check function signature with `get_function_doc()`.\n\n### Step 7: Full Build Validation\n\nOnce type checking passes, run the full build:\n```bash\nnpx ikas-component build\n```\n\nThis runs type checking + esbuild compilation + CSS scoping.\n\n### Quick Reference: CLI Commands\n\n| Command | Purpose |\n|---------|--------|\n| `npx ikas-component config add-component --name X --type section --props '[...]'` | **Primary** — create component with all props |\n| `npx ikas-component config add-component --name X --type section --isHeader --props '[...]'` | Create header section |\n| `npx ikas-component config add-component --name X --type section --isFooter --props '[...]'` | Create footer section |\n| `npx ikas-component config add-component --name X --type section` | Create component with no props |\n| `npx ikas-component config add-prop --component X --name y --displayName Y --type TEXT` | Add a prop incrementally |\n| `npx ikas-component config update-prop --component X --prop y --required true` | Update a prop |\n| `npx ikas-component config remove-prop --component X --prop y` | Remove a prop |\n| `npx ikas-component config remove-component --name X` | Remove a component |\n| `npx ikas-component config list` | List all components and props |\n| `npx ikas-component check --json` | Type-check with JSON output |\n| `npx ikas-component build` | Full production build |\n\n### Quick Reference: MCP Tools\n\n| Tool | When to Use |\n|------|------------|\n| `get_section_template(type)` | Starting a new section — get API integration pattern reference + CLI command (create original design, don't copy layout) |\n| `get_framework_guide(topic)` | Understanding patterns, pitfalls, architecture |\n| `get_function_doc(name)` | Looking up exact function signature |\n| `get_model_guide(type)` | Working with a model type (IkasProduct, IkasCart, etc.) |\n| `get_prop_types()` | Checking available prop types for ikas.config.json |\n| `search_docs(query)` | Finding relevant functions/docs by keyword |",
227
+ "content": "## AI Agent Workflow\n\nThis is the recommended step-by-step workflow for AI agents building ikas code components. Follow these steps in order for reliable, error-free results.\n\n**IMPORTANT: NEVER create or edit `types.ts` manually — it is auto-generated by the CLI.** The CLI commands below update BOTH `ikas.config.json` AND `types.ts` automatically.\n\n### Step 1: Create Component with Props\n\nUse `get_section_template(sectionType)` to get a starter template — it includes a ready-to-run CLI command with `--props`.\n\n**Important:** When creating a section, always include a `backgroundColor` COLOR prop (default: `#ffffff`). Optionally consider adding `textColor` for text elements directly on the section background.\n\n**Important:** When creating a header or footer section, use `--isHeader` or `--isFooter` flags on the `add-component` command. This marks the section as the store's common header/footer so it appears on all pages automatically:\n```bash\nnpx ikas-component config add-component --name \"Header\" --type section --isHeader --props '[...]'\nnpx ikas-component config add-component --name \"Footer\" --type section --isFooter --props '[...]'\n```\n\n**Important:** Ensure ALL user-visible text (headings, buttons, labels, empty states, loading text) is exposed as TEXT props. Never hardcode text strings in JSX. Use `defaultValue` for English defaults. For button loading states, use two separate props (e.g., `submitButtonText` + `submittingButtonText`). Group text props under a \"Texts\" propGroup when 5+ props exist.\n\nThen run the single `add-component --props` command to create the component scaffold WITH all props in one step:\n```bash\nnpx ikas-component config add-component --name \"HeroSection\" --type section --props '[{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\",\"required\":true},{\"name\":\"backgroundImage\",\"displayName\":\"Background Image\",\"type\":\"IMAGE\"},{\"name\":\"showButton\",\"displayName\":\"Show Button\",\"type\":\"BOOLEAN\"}]'\n```\n\nThis creates the component directory with `index.tsx`, `types.ts` (with correct Props interface), `styles.css`, updates `ikas.config.json`, and updates the barrel export. The output is JSON:\n```json\n{\"success\": true, \"componentId\": \"abc123-hero-section\", \"componentName\": \"HeroSection\", \"type\": \"section\", \"propsCount\": 3, \"files\": [...]}\n```\n\nThe `--props` flag accepts a JSON array of prop objects. Each prop needs `name` + `type` at minimum. `displayName` is auto-generated from camelCase name if omitted (e.g. `backgroundImage` → `\"Background Image\"`).\n\nTo add more props later, use `add-prop`:\n```bash\nnpx ikas-component config add-prop --component \"HeroSection\" --name \"subtitle\" --displayName \"Subtitle\" --type TEXT\n```\n\n### Step 2: Get Reference Material\n\nBefore writing component code, use MCP tools to get the right patterns:\n- `get_section_template(sectionType)` — Get the API integration pattern reference for common section types (product-detail, cart, login, header, footer, etc.). Study the function calls and data access patterns only.\n- `get_framework_guide(\"common-pitfalls\")` — Review common mistakes to avoid\n- `get_framework_guide(\"component-structure\")` — Review component structure patterns\n- `get_function_doc(functionName)` — Look up exact function signatures before using them\n- `get_model_guide(typeName)` — Get comprehensive info about a model type\n\n### Step 2.5: Design Your Component (IMPORTANT)\n\nBefore writing code, plan an ORIGINAL visual design:\n- Do NOT reproduce layout, CSS class names, or HTML structure from templates/examples\n- Templates and examples demonstrate correct API usage only — they are not designs to copy\n- **KEEP from references:** imports, function calls, data access patterns, form handling flows, type casts, null safety patterns\n- **CREATE fresh:** all JSX/HTML structure, all CSS class names and rules, all visual layout and spacing, all UX patterns (loading states, empty states, transitions, responsive breakpoints)\n- Think about the specific store's brand and design language when creating your component\n\n### Step 3: Write the Component Code\n\nEdit `src/components/{ComponentName}/index.tsx` with the implementation. **Do NOT edit `types.ts`** — it was already generated correctly in Step 1. Key rules:\n- Import props from `./types` (auto-generated in Step 1)\n- Import storefront functions from `@ikas/bp-storefront`\n- Root export should use named + default export: `export function X({ ... }: Props) { ... }` + `export default X;`\n- Only wrap sub-components with `observer()` when they independently read MobX stores\n- Use optional chaining for all props: `props.title ?? \"Default\"`\n- Use `as unknown as boolean` cast for functions like `hasProductStock`, `isAddToCartEnabled`\n- **Sections MUST apply backgroundColor**: destructure `backgroundColor = \"#ffffff\"` and add `style={backgroundColor ? { backgroundColor } : undefined}` on the root `<section>` element\n\n### Step 3.5: Organize Props into Groups (if 5+ props)\n\nIf the component has 5 or more props, organize them into groups for better editor UX:\n```bash\nnpx ikas-component config add-prop-group --component \"HeroSection\" --id content --name \"Content\"\nnpx ikas-component config add-prop-group --component \"HeroSection\" --id appearance --name \"Appearance\"\nnpx ikas-component config update-prop --component \"HeroSection\" --prop title --group content\nnpx ikas-component config update-prop --component \"HeroSection\" --prop backgroundImage --group appearance\n```\n\n### Step 3.6: Hide Props That Do Not Apply (optional)\n\nWhen a prop only makes sense while another setting is on, gate it instead of leaving it in the panel\nall the time. A prop or a whole group can be conditioned on a `BOOLEAN` or `ENUM` prop of the same\ncomponent — the control is drawn only while the condition holds, and a `required` prop hidden this\nway stops blocking a publish (its value is still stored and still passed to your component):\n```bash\n# badgeText shows only while the showBadge toggle is on\nnpx ikas-component config update-prop --component \"HeroSection\" --prop badgeText --requiredProp showBadge\n# the whole \"Badge\" group follows the same toggle\nnpx ikas-component config update-prop-group --component \"HeroSection\" --id badge --requiredProp showBadge\n# columns shows only for the \"grid\" option of the layout ENUM prop\nnpx ikas-component config update-prop --component \"HeroSection\" --prop columns --requiredProp layout --requiredEnumOptionValue grid\n```\nSee `get_framework_guide(\"conditional-visibility\")` for the rules (the CLI rejects self-gates,\ncycles, non-BOOLEAN/ENUM governing props, and a group gated by a prop inside it).\n\n### Step 4: Write Styles\n\nEdit `src/components/{ComponentName}/styles.css`. Key rules:\n- Use class selectors only (`.my-class`), never bare element selectors\n- CSS is auto-scoped at build time — no manual namespacing needed\n- Keep a sub-component's CSS in its OWN folder's `styles.css`. Every command (`dev`, `publish`, `build`) collects the sibling `styles.css` of each imported sub-component automatically. Never copy a sub-component's rules into the parent's `styles.css`, and never `import \"./styles.css\"` from TSX.\n- For sections: `width: 100%; padding: 64px 24px;` with inner max-width container\n\n### Step 5: Verify with Type Checking\n\nRun the lightweight type checker:\n```bash\nnpx ikas-component check --json\n```\n\nSuccess output:\n```json\n{\"success\": true, \"errors\": []}\n```\n\nError output:\n```json\n{\"success\": false, \"errorCount\": 2, \"errors\": [{\"file\": \"src/components/Hero/index.tsx\", \"line\": 15, \"column\": 3, \"code\": \"TS2339\", \"message\": \"Property 'title' does not exist on type 'Props'\"}]}\n```\n\n### Step 6: Fix Errors and Re-check\n\nFor each error:\n1. Read the file and line number from the error\n2. Fix the issue (common fixes below)\n3. Re-run `npx ikas-component check --json`\n\n**Common error fixes:**\n- `Property 'x' does not exist on type 'Props'` — Prop wasn't added via CLI. Run `npx ikas-component config add-prop`.\n- `Cannot find module '@ikas/bp-storefront'` — Normal in type-check if node_modules not fully installed. Focus on component-level errors.\n- `Type 'X' is not assignable to type 'Y'` — Check function signature with `get_function_doc()`.\n\n### Step 7: Full Build Validation\n\nOnce type checking passes, run the full build:\n```bash\nnpx ikas-component build\n```\n\nThis runs type checking + esbuild compilation + CSS scoping. It catches what `check` cannot: deep `@ikas/bp-storefront/dist/...` imports and disallowed external packages are esbuild-level errors, invisible to `tsc`.\n\n`build` is a VALIDATION step only — nothing reads its `dist/` output. The editor gets components over the `dev` WebSocket (`import_section`), and releases go out via `publish`. So do NOT restructure source code to make `dist/` look a certain way, and do not treat a difference between `dist/` and the editor's copy as a bug: `build` compiles each component in isolation, so by design it has no shared chunks, no `canvasClientJs`, and no `global.css`.\n\n### Quick Reference: CLI Commands\n\n| Command | Purpose |\n|---------|--------|\n| `npx ikas-component config add-component --name X --type section --props '[...]'` | **Primary** — create component with all props |\n| `npx ikas-component config add-component --name X --type section --isHeader --props '[...]'` | Create header section |\n| `npx ikas-component config add-component --name X --type section --isFooter --props '[...]'` | Create footer section |\n| `npx ikas-component config add-component --name X --type section` | Create component with no props |\n| `npx ikas-component config add-prop --component X --name y --displayName Y --type TEXT` | Add a prop incrementally |\n| `npx ikas-component config update-prop --component X --prop y --required true` | Update a prop |\n| `npx ikas-component config remove-prop --component X --prop y --yes` | Remove a prop (only after the user agreed) |\n| `npx ikas-component config remove-component --name X --yes` | Remove a component and its folder (only after the user agreed) |\n| `npx ikas-component config list` | List all components and props |\n| `npx ikas-component check --json` | Type-check with JSON output |\n| `npx ikas-component build` | Full production build |\n\n### Quick Reference: MCP Tools\n\n| Tool | When to Use |\n|------|------------|\n| `get_section_template(type)` | Starting a new section — get API integration pattern reference + CLI command (create original design, don't copy layout) |\n| `get_framework_guide(topic)` | Understanding patterns, pitfalls, architecture |\n| `get_function_doc(name)` | Looking up exact function signature |\n| `get_model_guide(type)` | Working with a model type (IkasProduct, IkasCart, etc.) |\n| `get_prop_types()` | Checking available prop types for ikas.config.json |\n| `search_docs(query)` | Finding relevant functions/docs by keyword |\n\n### Reuse the theme's global settings\n\nBefore hardcoding colors or typography, call `list_theme_globals` to discover the theme's existing global variables and design tokens, and read them at runtime via `getThemeColors()` / `getThemeSetting()` / `getThemeTypography()` from `@ikas/bp-storefront`. You can also create new ones with `create_theme_global`. See `get_framework_guide(\"theme-globals\")`.",
213
228
  "tags": [
214
229
  "ai",
215
230
  "workflow",
@@ -217,39 +232,37 @@
217
232
  "cli",
218
233
  "steps",
219
234
  "guide",
220
- "automation"
235
+ "automation",
236
+ "requiredProp",
237
+ "conditional visibility"
221
238
  ]
222
239
  },
223
240
  "form-handling": {
224
241
  "title": "Form Handling",
225
242
  "description": "How to use the form model pattern for login, registration, address, and other forms",
226
- "content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\n// Root export — no observer() needed, ikas runtime handles reactivity via autorun()\nexport function LoginForm() {\n const loginForm = customerStore.loginForm;\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(\"/account\");\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n {loginForm.isFailure && <div>{loginForm.responseMessage}</div>}\n\n <input\n type=\"email\"\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, e.target.value)}\n />\n {loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n\n <input\n type=\"password\"\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, e.target.value)}\n />\n {loginForm.password.hasError && <span>{loginForm.password.message}</span>}\n\n <button type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n );\n}\n\nexport default LoginForm;\n```\n\n### Available Form Types\n\n| Form | Init | Setter Functions | Submit |\n|------|------|-----------------|--------|\n| Login | `initLoginForm(form)` | `setLoginFormEmail()`, `setLoginFormPassword()` | `submitLoginForm(form)` |\n| Register | `initRegisterForm(form)` | `setRegisterFormEmail()`, `setRegisterFormPassword()`, `setRegisterFormFirstName()`, `setRegisterFormLastName()` | `submitRegisterForm(form)` |\n| Forgot Password | `initForgotPasswordForm(form)` | `setForgotPasswordFormEmail()` | `submitForgotPasswordForm(form)` |\n| Address | `initAddressForm(form)` | `setAddressFormField()` for each field | `submitAddressForm(form)` |\n| Contact | `initContactForm(form)` | `setContactFormField()` for each field | `submitContactForm(form)` |\n| Account Info | `initAccountInfoForm(form)` | `setAccountInfoFormField()` for each field | `submitAccountInfoForm(form)` |\n| Newsletter | `initNewsletterSubscriptionForm(form)` | `setNewsletterEmail()` | `submitNewsletterSubscriptionForm(form)` |\n| Coupon Code | — | `setCouponCode()` | `applyCouponCode()` |\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
243
+ "content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\n// Root export — no observer() needed, ikas runtime handles reactivity via autorun()\nexport function LoginForm() {\n // Forms are reached through get*Form(customerStore) accessors — the store\n // holds them in private fields (`_loginForm`), so `customerStore.loginForm`\n // does not compile.\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(\"/account\");\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n {loginForm.isFailure && <div>{loginForm.responseMessage}</div>}\n\n <input\n type=\"email\"\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, e.target.value)}\n />\n {loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n\n <input\n type=\"password\"\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, e.target.value)}\n />\n {loginForm.password.hasError && <span>{loginForm.password.message}</span>}\n\n <button type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n );\n}\n\nexport default LoginForm;\n```\n\n### Available Form Types\n\n| Form | Init | Setter Functions | Submit |\n|------|------|-----------------|--------|\n| Login | `initLoginForm(form)` | `setLoginFormEmail()`, `setLoginFormPassword()` | `submitLoginForm(form)` |\n| Register | `initRegisterForm(form)` | `setRegisterFormEmail()`, `setRegisterFormPassword()`, `setRegisterFormFirstName()`, `setRegisterFormLastName()` | `submitRegisterForm(form)` |\n| Forgot Password | `initForgotPasswordForm(form)` | `setForgotPasswordFormEmail()` | `submitForgotPasswordForm(form)` |\n| Address | `initAddressForm(form)` | `setAddressFormField()` for each field | `submitAddressForm(form)` |\n| Contact | `initContactForm(form)` | `setContactFormField()` for each field | `submitContactForm(form)` |\n| Account Info | `initAccountInfoForm(form)` | `setAccountInfoFormField()` for each field | `submitAccountInfoForm(form)` |\n| Newsletter | `initNewsletterSubscriptionForm(form)` | `setNewsletterEmail()` | `submitNewsletterSubscriptionForm(form)` |\n| Coupon Code | `getCouponCodeForm(customerStore)` → `initCouponCodeForm(form)` | `setCouponCodeFormCouponCode(form, value)` | `submitCouponCodeForm(form)` (remove with `removeCouponCodeForm(form)`) |\n| Deactivate Customer | `getDeactivateCustomerForm(customerStore)` → `initDeactivateCustomerForm(form)` | `setDeactivateCustomerFormPassword(form, value)` | `submitDeactivateCustomerForm(form)` (call `logout(customerStore)` after success) |\n\n**Address is the exception to this page.** It follows the same init/set/submit contract, but its\nfield set and row layout are resolved from the selected country at runtime via\n`addressForm.addressFormat` -- you cannot lay it out statically. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building any address form.\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
227
244
  "tags": [
228
245
  "form",
229
246
  "login",
230
247
  "register",
231
248
  "address",
232
249
  "validation",
233
- "input"
250
+ "input",
251
+ "password",
252
+ "account",
253
+ "deactivate"
234
254
  ]
235
255
  },
236
256
  "async-data-patterns": {
237
257
  "title": "Async Data & Loading Patterns",
238
258
  "description": "How to handle async operations, loading states, and store data in ikas components",
239
- "content": "## Async Data & Loading Patterns\n\n### 1. Store State is Null Until Initialized\n\n**Note:** Root component exports are automatically reactive via the ikas runtime's `autorun()` — no `observer()` needed. The examples below show `observer()` on **sub-components** that independently read store data.\n\nStore data (`cartStore.cart`, `customerStore.customer`) is `null` until the store initializes. Use null-safe access:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n // cart is null until initialized — use optional chaining\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n```\n\n### 2. Loading Pattern for Async Operations\n\nUse `useState` for local loading flags. Always use `try/finally` to clear the loading state:\n\n```tsx\nimport { useState } from \"preact/hooks\";\nimport { addItemToCart } from \"@ikas/bp-storefront\";\n\nconst [isLoading, setIsLoading] = useState(false);\n\nconst handleAddToCart = async () => {\n if (isLoading) return; // prevent double-click\n setIsLoading(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsLoading(false);\n }\n};\n\n// In JSX:\n<button disabled={isLoading} onClick={handleAddToCart}>\n {isLoading ? \"Adding...\" : \"Add to Cart\"}\n</button>\n```\n\n### 3. Cart Operation Results\n\n`addItemToCart` returns a result object. Check for validation errors:\n\n```tsx\nconst result = await addItemToCart(variant, product, 1);\nif (result.success) {\n // Item added successfully\n} else if (result.validationError === \"INSUFFICIENT_STOCK\") {\n // Not enough stock\n} else if (result.validationError === \"INVALID_PRODUCT_OPTION_VALUES\") {\n // Variant options not fully selected\n}\n```\n\n### 4. Form Submission Results\n\nForm submit functions return `boolean` and set status flags on the form:\n\n```tsx\nconst success = await submitLoginForm(loginForm);\nif (success) {\n // loginForm.isSuccess is true\n Router.navigate(\"/account\");\n} else {\n // loginForm.isFailure is true\n // loginForm.responseMessage has the error message\n console.log(loginForm.responseMessage);\n}\n```\n\n### 5. Observer Auto Re-rendering\n\nStore updates automatically trigger re-renders. Root components get this via `autorun()`, sub-components via `observer()`. You do NOT need manual state management for store-derived values:\n\n```tsx\n// WRONG — unnecessary useState for store data\nconst [cartItems, setCartItems] = useState([]);\nuseEffect(() => { setCartItems(cartStore.cart?.orderLineItems ?? []); }, [cartStore.cart]);\n\n// CORRECT — observer sub-component for independent reactivity\nconst CartList = observer(function CartList() {\n const items = cartStore.cart?.orderLineItems ?? [];\n return (\n <div>\n {items.map((item) => (\n <div key={item.id}>{item.variant?.name}</div>\n ))}\n </div>\n );\n});\n\n// Root export uses CartList — no observer needed on root\nexport function CartPage({ title }: Props) {\n return (\n <section>\n <h1>{title}</h1>\n <CartList />\n </section>\n );\n}\n\nexport default CartPage;\n```\n\n### 6. Pagination / Data Fetching\n\nList stores (blog, brand, category) use async pagination functions:\n\n```tsx\nimport { hasBlogListNextPage, getBlogListNextPage } from \"@ikas/bp-storefront\";\n\n// Check if more pages exist\nif (hasBlogListNextPage(blogList)) {\n // Fetch next page — mutates blogList in place, observer re-renders\n await getBlogListNextPage(blogList);\n}\n```\n\nThese functions mutate the list model in place. If your component uses `observer()`, it will re-render automatically when new data arrives.",
240
- "tags": [
241
- "async",
242
- "loading",
243
- "error",
244
- "stores",
245
- "cart",
246
- "fetch"
247
- ]
259
+ "content": "## Async Data & Loading Patterns\n\n### 1. Store State is Null Until Initialized\n\n**Note:** Root component exports are automatically reactive via the ikas runtime's `autorun()` — no `observer()` needed. The examples below show `observer()` on **sub-components** that independently read store data.\n\nStore data (`cartStore.cart`, `customerStore.customer`) is `null` until the store initializes. Use null-safe access:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n // cart is null until initialized — use optional chaining\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n```\n\n### 2. Loading Pattern for Async Operations\n\nUse `useState` for local loading flags. Always use `try/finally` to clear the loading state:\n\n```tsx\nimport { useState } from \"preact/hooks\";\nimport { addItemToCart } from \"@ikas/bp-storefront\";\n\nconst [isLoading, setIsLoading] = useState(false);\n\nconst handleAddToCart = async () => {\n if (isLoading) return; // prevent double-click\n setIsLoading(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsLoading(false);\n }\n};\n\n// In JSX:\n<button disabled={isLoading} onClick={handleAddToCart}>\n {isLoading ? \"Adding...\" : \"Add to Cart\"}\n</button>\n```\n\n### 3. Cart Operation Results\n\n`addItemToCart` returns a result object. Check for validation errors:\n\n```tsx\nconst result = await addItemToCart(variant, product, 1);\nif (result.success) {\n // Item added successfully\n} else if (result.validationError === \"INSUFFICIENT_STOCK\") {\n // Not enough stock\n} else if (result.validationError === \"INVALID_PRODUCT_OPTION_VALUES\") {\n // Variant options not fully selected\n}\n```\n\n### 4. Form Submission Results\n\nForm submit functions return `boolean` and set status flags on the form:\n\n```tsx\nconst success = await submitLoginForm(loginForm);\nif (success) {\n // loginForm.isSuccess is true\n Router.navigate(\"/account\");\n} else {\n // loginForm.isFailure is true\n // loginForm.responseMessage has the error message\n console.log(loginForm.responseMessage);\n}\n```\n\n### 5. Observer Auto Re-rendering\n\nStore updates automatically trigger re-renders. Root components get this via `autorun()`, sub-components via `observer()`. You do NOT need manual state management for store-derived values:\n\n```tsx\n// WRONG — unnecessary useState for store data\nconst [cartItems, setCartItems] = useState([]);\nuseEffect(() => { setCartItems(cartStore.cart?.orderLineItems ?? []); }, [cartStore.cart]);\n\n// CORRECT — observer sub-component for independent reactivity\nconst CartList = observer(function CartList() {\n const items = cartStore.cart?.orderLineItems ?? [];\n return (\n <div>\n {items.map((item) => (\n <div key={item.id}>{item.variant?.name}</div>\n ))}\n </div>\n );\n});\n\n// Root export uses CartList — no observer needed on root\nexport function CartPage({ title }: Props) {\n return (\n <section>\n <h1>{title}</h1>\n <CartList />\n </section>\n );\n}\n\nexport default CartPage;\n```\n\n### 6. Pagination / Data Fetching\n\nList stores (blog, brand, category) use async pagination functions:\n\n```tsx\nimport { hasBlogListNextPage, getBlogListNextPage } from \"@ikas/bp-storefront\";\n\n// Check if more pages exist\nif (hasBlogListNextPage(blogList)) {\n // Fetch next page — mutates blogList in place, observer re-renders\n await getBlogListNextPage(blogList);\n}\n```\n\nThese functions mutate the list model in place. If your component uses `observer()`, it will re-render automatically when new data arrives.\n\nTo batch-fetch full products by id — e.g. enriching cart lines with the `salesChannels` limits or per-variant `stock` they don't carry — use `bs_searchProductsById(baseStore, { productIds })`: one request for all ids. See the \"Enforcing Limits on Cart Lines\" section in the `cart-patterns` guide for the full pattern.",
260
+ "tags": ["async", "loading", "error", "stores", "cart", "fetch"]
248
261
  },
249
262
  "product-detail-patterns": {
250
263
  "title": "Product Detail Patterns (Serel Reference)",
251
264
  "description": "Serel ProductDetail with 12 child components in two COMPONENT_LIST slots, ProductGallery local component, Breadcrumb navigation, and component slot architecture",
252
- "content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n### Key Pattern: Multiple COMPONENT_LIST Slots\n\nUsing two slots (`components` and `bottomComponents`) allows the store owner to arrange child components differently:\n- `components` — displayed next to the gallery in a side panel\n- `bottomComponents` — displayed full-width below the gallery\n\nThis pattern can be applied to any section that needs multiple layout regions.",
265
+ "content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n### Stock Locations (informational)\n\n`getProductAvailableStockLocations(product)` and the `IkasStockLocation*` types expose per-location availability. These are **not** part of the standard product-detail purchase flow: the reference theme does not use them on the PDP, and fulfillment (which location ships or serves an order) is resolved at checkout. Build a \"find in store\" or pickup surface from them only if your store explicitly needs one; otherwise you can ignore them on the PDP.\n\n**Returns `IkasProductStockLocations[]`** — an array of `{ stockCount, stockLocation, variantId }` rows. Compatibility note: older SDK builds (before the fix) resolved this to a `{ stockLocations: [...] }` wrapper object instead of a bare array, so a direct `.filter`/`.map` could throw `(...).filter is not a function`. To stay safe across SDK versions, unwrap defensively:\n\n```tsx\nconst result = await getProductAvailableStockLocations(product);\nconst rows = Array.isArray(result)\n ? result\n : ((result as { stockLocations?: IkasProductStockLocations[] })?.stockLocations ?? []);\n```\n\n### Key Pattern: Multiple COMPONENT_LIST Slots\n\nUsing two slots (`components` and `bottomComponents`) allows the store owner to arrange child components differently:\n- `components` — displayed next to the gallery in a side panel\n- `bottomComponents` — displayed full-width below the gallery\n\nThis pattern can be applied to any section that needs multiple layout regions.",
253
266
  "tags": [
254
267
  "product",
255
268
  "detail",
@@ -264,6 +277,36 @@
264
277
  "serel"
265
278
  ]
266
279
  },
280
+ "back-in-stock-patterns": {
281
+ "title": "Back-in-Stock Notification Patterns",
282
+ "description": "Notify-me-when-available flow for out-of-stock variants: the login-required vs email-form branches, the notification form lifecycle, and the persistent reminder-saved state",
283
+ "content": "## Back-in-Stock Notification Patterns\n\nWhen a variant is out of stock, the add-to-cart CTA is a dead end unless you offer a \"notify me when it's back\" flow. ikas ships the full API for this, wired up in the `ProductDetailBackInStock` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailBackInStock\", kind: \"children\" })\n```\n\n### When to show it\n\nRender the notify-me UI only when the variant is out of stock AND back-in-stock is enabled — otherwise render nothing and let `ProductDetailAddToCart` own the CTA:\n\n```tsx\nconst selectedVariant = getSelectedProductVariant(product);\nconst isOutOfStock = !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\nif (!isOutOfStock) return null;\nif (!getProductVariantIsBackInStockEnabled(selectedVariant)) return null;\n```\n\n### Two branches\n\n`getProductVariantIsBackInStockCustomerLoginRequired(variant)` selects the flow:\n\n- **Login required** — a logged-in customer is registered immediately with their own email; an anonymous visitor is sent to login first.\n- **No login required** — collect an email through the notification form.\n\n```tsx\nconst handleNotify = async () => {\n if (getProductVariantIsBackInStockCustomerLoginRequired(selectedVariant)) {\n const email = customerStore.customer?.email;\n if (email) {\n await saveProductVariantBackInStockReminder(selectedVariant, email);\n } else {\n Router.navigateToPage(\"LOGIN\");\n }\n return;\n }\n setEmailModalOpen(true); // anonymous email form\n};\n```\n\n### The email form (form lifecycle)\n\nThe no-login branch uses the standard ikas form model — get the form, init once, drive the input, submit:\n\n```tsx\nconst form = getProductVariantBackInStockForm(variant);\nuseEffect(() => { initBackInStockNotificationForm(form, variant); }, [form, variant]);\n\n<Input\n value={form.email?.value ?? \"\"}\n onInput={(e) => setBackInStockNotificationFormEmail(form, (e.target as HTMLInputElement).value)}\n/>\n\n// returns boolean; sets form.isSubmitting / isSuccess / isFailure / responseMessage\nconst ok = await submitBackInStockNotificationForm(form);\n```\n\nWrap the form component in `observer()` so it re-renders as the customer types and as the status flags flip — it reads the MobX-backed form.\n\n### Persistent \"saved\" state\n\n`variant.isBackInStockReminderSaved` is `true` once a reminder exists for that variant. Seed your CTA from it, and reset when the selected variant changes:\n\n```tsx\nconst [saved, setSaved] = useState(false);\nuseEffect(() => { setSaved(!!selectedVariant?.isBackInStockReminderSaved); }, [selectedVariant?.id]);\n```\n\nShow a non-interactive \"we'll notify you\" label instead of the button when `saved` is true.\n\n### Function reference\n\n| Function | Purpose |\n|----------|---------|\n| `getProductVariantIsBackInStockEnabled(variant)` | Gate: is back-in-stock enabled for this variant |\n| `getProductVariantIsBackInStockCustomerLoginRequired(variant)` | Branch: login-required vs email-form |\n| `saveProductVariantBackInStockReminder(variant, email)` | Register a reminder directly (login branch) |\n| `getProductVariantBackInStockForm(variant)` | Get-or-init the notification form model |\n| `initBackInStockNotificationForm(form, variant)` | Reset form fields + validation flags |\n| `setBackInStockNotificationFormEmail(form, value)` | Update the email field (re-validates after first submit) |\n| `submitBackInStockNotificationForm(form)` | Validate + save; returns boolean, sets status flags |\n| `variant.isBackInStockReminderSaved` | Persisted flag: a reminder already exists |",
284
+ "tags": [
285
+ "back-in-stock",
286
+ "notify-me",
287
+ "out-of-stock",
288
+ "variant",
289
+ "form",
290
+ "reminder",
291
+ "product-detail",
292
+ "conversion"
293
+ ]
294
+ },
295
+ "product-options": {
296
+ "title": "Product Options (Customization) Patterns",
297
+ "description": "Personalization option sets: async load, type-aware rendering for all option types, conditional child options, file upload, and submit-time validation with reset",
298
+ "content": "## Product Options (Customization) Patterns\n\nProduct options let customers personalize a product before adding to cart — text, checkbox, choice, color/date picker, and file uploads, including parent/child (conditional) options and per-option prices. The reference theme implements the whole matrix in the `ProductDetailOptionSet` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailOptionSet\", kind: \"children\" })\n```\n\n### Load, then render displayed options\n\nThe option set is fetched async — render nothing until it resolves, then render only the currently displayed options (child options appear or hide based on the parent selection):\n\n```tsx\nuseEffect(() => { if (product) getProductOptionSet(product).then(() => setLoaded(true)); }, [product]);\n\nconst optionSet = product.productOptionSet;\nconst displayed = getDisplayedOptions(optionSet) as IkasProductOption[];\n```\n\n### Type-aware rendering\n\nEach option has a type — do not render everything as a text input. Switch on the SDK type guards and render a dedicated control per type:\n\n| Guard | Control |\n|-------|---------|\n| `isTextOption` / `isTextAreaOption` | text input / textarea |\n| `isCheckboxOption` | checkbox |\n| `isChoiceOption` | choice (box / select / swatch by display style) |\n| `isColorPickerOption` | color picker |\n| `isDatePickerOption` | date picker |\n| `isFileOption` | file upload |\n\nChild options render recursively from `getDisplayedChildOptions(option)`.\n\n### File upload\n\nFile options carry extension and min/max-count constraints. Upload through the SDK and surface its errors (file size, file type, max files):\n\n```tsx\nawait productOptionFileUpload(option, files);\n```\n\n### Validation + reset\n\nValidate on submit (used by add-to-cart), and reset after a successful add:\n\n```tsx\n// in add-to-cart, before adding:\nif (!validateOptionSet(product.productOptionSet, errorText)) return; // dispatches \"ikas:show-option-errors\"\n\n// after a successful add:\ninitProductOptionSetValues(product.productOptionSet);\nwindow.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n```\n\nThe option-set component listens for `ikas:show-option-errors` / `ikas:reset-option-state` to toggle its error display; validation itself is `hasValidProductOptionSetValues(...)` under the hood.\n\n### Key rules\n\n1. Gate render on the async `getProductOptionSet(product)` resolving.\n2. Render from `getDisplayedOptions(...)`, never a hardcoded list — child visibility is dynamic.\n3. Switch on the type guards; each type has its own control.\n4. Validate with `validateOptionSet` at add-to-cart; reset with `initProductOptionSetValues` after a successful add.",
299
+ "tags": [
300
+ "product-options",
301
+ "customization",
302
+ "personalization",
303
+ "option-set",
304
+ "file-upload",
305
+ "child-options",
306
+ "validation",
307
+ "product-detail"
308
+ ]
309
+ },
267
310
  "product-list-patterns": {
268
311
  "title": "Product List & Filtering Patterns",
269
312
  "description": "Category pages, product filtering with display-type-aware rendering (swatch, number range, checkbox), sorting, pagination, and search patterns",
@@ -283,7 +326,7 @@
283
326
  "cart-patterns": {
284
327
  "title": "Cart Management Patterns (Serel Reference)",
285
328
  "description": "Serel CartPage section with CartItem sub-component, OrderSummary/CouponCode/EmptyState local components, cartStore patterns, and checkout flow",
286
- "content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nHandles coupon code input and submission:\n\n```tsx\nimport { initCouponCodeForm, setCouponCode, submitCouponCodeForm } from \"@ikas/bp-storefront\";\n\nfunction CouponCode({ cart }) {\n useEffect(() => { initCouponCodeForm(cart); }, []);\n const [code, setCode] = useState(\"\");\n\n const handleApply = async () => {\n setCouponCode(cart, code);\n await submitCouponCodeForm(cart);\n };\n\n return (\n <div className=\"coupon\">\n <input value={code} onInput={(e) => setCode((e.target as HTMLInputElement).value)} />\n <button onClick={handleApply}>Apply</button>\n </div>\n );\n}\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
329
+ "content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Quantity Limits (min/max per cart)\n\nA product's sales channel can cap how many units go into one cart. The limits live on the product's `salesChannels`, matched to the active channel:\n\n```tsx\nimport { IkasStorefrontConfig } from \"@ikas/bp-storefront\";\n\nconst channel = product.salesChannels?.find((sc) => sc.id === IkasStorefrontConfig.salesChannelId);\nconst min = channel?.minQuantityPerCart ?? 1;\nconst max = channel?.maxQuantityPerCart; // null/undefined = no cap\n```\n\nApply these to the quantity stepper: seed the initial quantity to `min`, and cap increments at `max`. `addItemToCart(variant, product, qty)` clamps to the same limits internally only when it creates a **new** cart line — and silently, so enforce them in the UI too or the customer sees the number snap with no explanation.\n\n> **The internal clamp only runs when a NEW cart line is created.** If the variant already has a line in the cart, `addItemToCart` delegates to `changeItemQuantity(existing, existing.quantity + qty)` before the clamp is reached — and `changeItemQuantity` never clamps. Cart page / cart drawer steppers therefore get no SDK enforcement at all; enforce the limits in the UI (see \"Enforcing Limits on Cart Lines\" below).\n\n> `IkasStorefrontConfig.salesChannelId` is a property. The older `getSalesChannelId()` method does not exist in this SDK.\n\n### Cart Operation validationError (complete list)\n\n`addItemToCart` / `changeItemQuantity` return `IkasCartOperationResult` — `{ success, validationError?, response? }`. The `validationError` union has exactly three values, but at runtime only `addItemToCart` ever sets it (client-side pre-validation before the API call). `changeItemQuantity` does no client-side validation and never returns a `validationError` — branch on `success` (and `response.errorCodes` for details), not on `validationError`, for cart-line quantity changes:\n\n| Value | Meaning |\n|-------|---------|\n| `INSUFFICIENT_STOCK` | Not enough stock for the requested quantity |\n| `INVALID_PRODUCT_OPTION_VALUES` | Required product options not fully or validly selected |\n| `EMPTY_CART` | The operation needs a non-empty cart (declared in the type union, but no current code path emits it) |\n\nQuantity limits are never surfaced as a `validationError` — there is **no** `MAX_QUANTITY_PER_CART_*` / `MIN_QUANTITY_PER_CART_*` value beyond the three above. They surface one level lower instead: when a mutation would push a line past `maxQuantityPerCart`, the API rejects it with the GraphQL error code `MAX_QUANTITY_PER_CART_LIMIT_REACHED` (an `extensions.code` value the API client copies into `response.errorCodes`). `addItemToCart` and `changeItemQuantity` go through the same mutation, so this is the reactive signal on the cart-line path — exactly where the SDK never clamps:\n\n```tsx\nimport { APIErrorCode, changeItemQuantity } from \"@ikas/bp-storefront\";\n\nconst result = await changeItemQuantity(item, qty);\nif (!result.success && result.response?.errorCodes?.includes(APIErrorCode.MAX_QUANTITY_PER_CART_LIMIT_REACHED)) {\n // The sales channel's per-cart cap rejected the quantity — tell the customer.\n}\n```\n\nTreat the error code as the fallback, not the UX: prevent limit violations proactively in the stepper (see \"Quantity Limits\" above and \"Enforcing Limits on Cart Lines\" below) so the customer never hits the rejection in the first place.\n\n### Enforcing Limits on Cart Lines\n\nCart lines cannot tell you the limits: `IkasOrderLineItem.variant` is an `IkasOrderLineVariant`, which carries **neither** `salesChannels` nor `stock` — both live only on `IkasProduct` / `IkasProductVariant`. To enforce `minQuantityPerCart` / `maxQuantityPerCart` (or stock caps) on the cart page / drawer, batch-fetch the line products once with `bs_searchProductsById` and map the limits by product id:\n\n```tsx\nimport { baseStore, bs_searchProductsById, cartStore, IkasStorefrontConfig } from \"@ikas/bp-storefront\";\n\n// Collect the distinct productIds of the cart lines and fetch them in ONE request:\nconst productIds = [\n ...new Set(\n (cartStore.cart?.orderLineItems ?? [])\n .map((item) => item.variant.productId)\n .filter((id): id is string => !!id)\n ),\n];\nconst { products } = await bs_searchProductsById(baseStore, { productIds });\n\nconst limitsByProductId = new Map<string, { min: number; max: number | null }>();\nfor (const product of products ?? []) {\n const channel = product.salesChannels?.find((sc) => sc.id === IkasStorefrontConfig.salesChannelId);\n limitsByProductId.set(product.id, {\n min: channel?.minQuantityPerCart ?? 1,\n max: channel?.maxQuantityPerCart ?? null,\n });\n}\n\n// In the line's stepper:\nconst limits = limitsByProductId.get(item.variant.productId ?? \"\");\nconst atMax = limits?.max != null && item.quantity >= limits.max;\n// disable the + button when atMax; block decrements below limits.min the same way\n```\n\n`bs_searchProductsById` returns full `IkasProduct` objects, so `salesChannels` (already filtered to the active sales channel) and per-variant `stock` (summed over the sales channel's configured stock locations) come in the same response — one fetch covers both quantity limits and stock caps. Fetch once per cart open and cache by product id rather than fetching per line; when lines render independently, collect the ids over a tick and flush a single batched request.\n\n### Campaign Offer Lines in the Cart\n\nCampaign discounts surface as order-level adjustment rows, not as edits on the line items. Format the adjustment amount with `formatCurrency` — the root export that applies the merchant's currency format settings:\n\n```tsx\nimport { formatCurrency } from \"@ikas/bp-storefront\";\n\n{cart.orderAdjustments?.map((adj) => (\n <div key={adj.name} className=\"cart-adjustment\">\n <span>{adj.name}</span>\n <span>{formatCurrency(adj.amount, adj.currencyCode, adj.currencySymbol)}</span>\n </div>\n))}\n```\n\nWhen a campaign auto-adds a bundled or gift line (a \"buy 1 get N\" offer), `isOrderLineItemAutoCreated(cart, item)` returns `true` for that line. Use it to badge the line and hide its quantity/remove controls — the customer cannot edit an auto-created line directly:\n\n```tsx\nimport { isOrderLineItemAutoCreated } from \"@ikas/bp-storefront\";\n\n{lineItems.map((item) => (\n <CartItem key={item.id} item={item} readOnly={isOrderLineItemAutoCreated(cart, item)} />\n))}\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
287
330
  "tags": [
288
331
  "cart",
289
332
  "checkout",
@@ -292,14 +335,41 @@
292
335
  "CouponCode",
293
336
  "cartStore",
294
337
  "quantity",
338
+ "quantity limits",
339
+ "maxQuantityPerCart",
340
+ "minQuantityPerCart",
341
+ "bs_searchProductsById",
295
342
  "sub-component",
296
- "serel"
343
+ "serel",
344
+ "currency",
345
+ "formatCurrency"
346
+ ]
347
+ },
348
+ "address-form-patterns": {
349
+ "title": "Address Form Patterns",
350
+ "description": "Country-driven address form layout: the addressFormat matrix, the renderFormField dispatch table, free-text fallback, and the country/state/city/district cascade",
351
+ "content": "## Address Form Patterns\n\nAddress forms are the one form type in ikas that you cannot lay out statically. The set of\nfields, their order, their grouping into rows, and whether each one is a `<select>` or a\nfree-text `<input>` are all driven by the selected country. Hardcoding a field list will\nproduce a form that is wrong for most of the world.\n\nEverything below is implemented end-to-end in the reference theme. Fetch the real file with:\n\n```\nget_section_child({ section: \"account-info-section\", name: \"AccountAddresses\", kind: \"children\" })\n```\n\n### 1. The layout matrix\n\n`addressForm.addressFormat` is an `AddressFormItem[][]`:\n\n- the **outer** array is rows, rendered top to bottom\n- the **inner** array is the fields sharing that row, rendered left to right\n\n```ts\n// A typical TR format:\n[[\"firstName\", \"lastName\"], [\"identityNumber\"], [\"phone\"],\n [\"addressLine1\"], [\"country\"], [\"state\", \"city\"], [\"district\", \"postalCode\"]]\n```\n\n`AddressFormItem` is a fixed union: `firstName`, `lastName`, `identityNumber`,\n`addressLine1`, `addressLine2`, `postalCode`, `country`, `state`, `city`,\n`district`, `region`, `phone`.\n\n**`title` is deliberately not in that union.** It is the address nickname (\"Home\", \"Work\"),\nit is not part of any postal format, and it is always required. Render it yourself, outside\nthe matrix.\n\n### 2. Where the matrix comes from\n\nYou never build `addressFormat` yourself. `initAddressForm()` resolves the country and\nloads the format and the per-field settings from the ikas location config. `setAddressFormCountry()`\nre-resolves both, so **switching country rewrites the layout**. Since the form is observable,\nyour component re-renders with the new matrix automatically -- as long as you render *from*\n`addressFormat` instead of from a list you wrote by hand.\n\nAlongside the matrix you get `addressForm.countrySettings`, which drives per-field\n`label`, `placeholder`, `isRequired`, and `isFreeText` -- already applied onto each field\nobject by the time `initAddressForm()` resolves. Read `field.label`; do not translate\naddress labels yourself.\n\n### 3. Reactivity: this is almost always a sub-component\n\nThe address form is a MobX observable. A **root** component export is already reactive via the\nikas runtime's `autorun()`, so it needs nothing. But an address form is usually a modal or a\npanel -- i.e. a **sub-component** -- and a sub-component that reads a MobX store must be wrapped\nin `observer()` or it will not re-render as the customer types or switches country:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX address form\nconst AddressModal = observer(function AddressModal({ address, onClose }: Props) {\n // ...\n});\n\nexport default AddressModal;\n```\n\nForgetting this is the single most common way an address form \"silently does nothing\":\nvalues never appear, and the country switch never re-lays-out the form.\n\n### 4. Creating the form\n\nTwo different constructors -- picking the wrong one is the most common mistake:\n\n```tsx\nimport { getEmptyAddressForm, getIkasCustomerAddressForm, customerStore } from \"@ikas/bp-storefront\";\n\n// New address: takes the STORE\nconst form = getEmptyAddressForm(customerStore);\n\n// Editing an existing address: takes the ADDRESS\nconst form = getIkasCustomerAddressForm(address);\n```\n\nThen initialize once, and gate render on it, because it does network work\n(country/state/city lists, location settings, checkout settings, shipping zones):\n\n```tsx\nconst [form] = useState(() => address ? getIkasCustomerAddressForm(address) : getEmptyAddressForm(customerStore));\nconst [isLoading, setIsLoading] = useState(true);\n\nuseEffect(() => {\n initAddressForm(form, address).then(() => setIsLoading(false));\n}, []);\n```\n\nRender a skeleton while `isLoading`. `addressFormat` is empty until init resolves, so\nrendering the matrix early yields a blank form.\n\n### 5. Rendering the matrix\n\n```tsx\nconst renderRows = (rows: AddressFormItem[][]) =>\n rows.map((row) => {\n // A field the current country does not use is absent from the form object entirely.\n const keys = row.filter((key) => form[key] != null);\n if (keys.length === 0) return null; // skip the row, do not leave a gap\n if (keys.length === 1) return <Fragment key={keys[0]}>{renderFormField(keys[0])}</Fragment>;\n return (\n <div key={keys.join(\"-\")} className=\"address-row\"> {/* multi-field row: flex/grid */}\n {keys.map((key) => <Fragment key={key}>{renderFormField(key)}</Fragment>)}\n </div>\n );\n });\n\n<form onSubmit={handleSubmit}>\n {renderInput(form.title, setAddressFormTitle, \"addr-title\")} {/* outside the matrix */}\n {renderRows(form.addressFormat ?? [])}\n</form>\n```\n\nAlways `?? []` -- `addressFormat` is optional and is undefined before init.\n\n### 6. The two leaf renderers\n\nEverything below builds on two small helpers. `renderInput` renders one text field from its\n`IkasFormItem`; note that the label, placeholder and error message all come off the field\nobject -- they are resolved per country, so do not hardcode or translate them yourself:\n\n```tsx\nconst renderInput = (\n field: IkasFormItem | undefined,\n setter: (form: AddressForm, value: string) => void,\n id: string,\n type?: string,\n) => {\n if (!field) return null;\n return (\n <FormItem\n label={field.label ?? \"\"}\n htmlFor={id}\n status={field.hasError ? \"error\" : \"default\"}\n helper={field.hasError ? field.message : undefined}\n >\n <Input\n id={id}\n type={type}\n value={field.value ?? \"\"}\n placeholder={field.placeholder}\n onInput={(e: Event) => setter(addressForm, (e.target as HTMLInputElement).value)}\n />\n </FormItem>\n );\n};\n```\n\n`renderSelect` is the same idea for the location fields, plus the free-text fallback -- see\nsection 8, which defines it.\n\n### 7. The dispatch table\n\n`renderFormField` maps a matrix key to (field object, setter, dom id). It makes **no layout\ndecision** -- that is the matrix's job. Keep it a flat switch:\n\n```tsx\nconst renderFormField = (key: AddressFormItem) => {\n switch (key) {\n case \"firstName\": return renderInput(form.firstName, setAddressFormFirstName, \"addr-first-name\");\n case \"lastName\": return renderInput(form.lastName, setAddressFormLastName, \"addr-last-name\");\n case \"phone\": return renderInput(form.phone, setAddressFormPhone, \"addr-phone\", \"tel\");\n case \"addressLine1\": return renderInput(form.addressLine1, setAddressFormAddressLine1, \"addr-line1\");\n case \"postalCode\": return renderInput(form.postalCode, setAddressFormPostalCode, \"addr-postal\");\n\n // Location fields are selects fed by *Options arrays:\n case \"country\": return renderSelect(form.country, form.countryOptions, setAddressFormCountry, \"addr-country\");\n case \"state\": return renderSelect(form.state, form.stateOptions, setAddressFormState, \"addr-state\");\n case \"city\": return renderSelect(form.city, form.cityOptions, setAddressFormCity, \"addr-city\");\n case \"district\": return renderSelect(form.district, form.districtOptions, setAddressFormDistrict, \"addr-district\");\n\n // region only exists when the resolved district has shipping-zone regions:\n case \"region\":\n return form.regionOptions?.length\n ? renderSelect(form.region, form.regionOptions, setAddressFormRegion, \"addr-region\")\n : null;\n default: return null;\n }\n};\n```\n\nEach `set*` helper takes `(form, value)` and re-validates automatically once the form has\nbeen submitted once, so error messages track typing after the first failed submit.\n\n### 8. Free-text fallback (required)\n\n`city` and `district` are `IkasFormFreeText`. Many countries have no city/district\nregistry, so the option list is empty and the field must degrade to a plain text input:\n\n```tsx\nconst renderSelect = (field, options, setter, id) => {\n if (!field) return null;\n // Either the country says \"type it\", or we simply have no options to offer.\n if (field.isFreeText || !options?.length) return renderInput(field, setter, id);\n return (\n <Select\n id={id}\n value={field.value ?? \"\"}\n disabled={field.isLoading}\n options={[{ value: \"\", label: field.placeholder ?? \"\" }, ...options]}\n onChange={(e) => setter(form, (e.target as HTMLSelectElement).value)}\n />\n );\n};\n```\n\nSkipping this check is what produces the classic \"empty dropdown, address cannot be saved\"\nbug on non-TR countries.\n\n### 9. The location cascade\n\n`country -> state -> city -> district -> region`. Each setter clears its dependents and\nloads the next list for you; the `*Options` arrays repopulate asynchronously. Two\nconsequences for your UI:\n\n- honor `field.isLoading` (disable the select while its list loads)\n- never cache option lists in local state -- read `form.*Options` on every render\n\nFree-text city/district are preserved across a state change rather than cleared, so a typed\nvalue is not lost.\n\n### 10. Validation and required flags\n\n`submitAddressForm(form)` validates and returns `boolean`; on failure each field carries\n`hasError` and `message`. Do not write your own address validation -- required-ness is\nresolved from the country format, the country field settings, and the merchant's checkout\nsettings (which can make identity number / postal code / phone mandatory or optional).\n\n`field.isRequired` is resolved for you; use it to mark labels with an asterisk.\nA field absent from `addressFormat` is never required.\n\n```tsx\nconst handleSubmit = async (e: Event) => {\n e.preventDefault();\n const ok = await submitAddressForm(form);\n if (ok) onSaved(); // form.responseMessage carries the server message\n};\n```\n\nDisable the submit button while `form.isSubmitting`.\n\n### 11. Deleting\n\n```tsx\nimport { deleteCustomerAddress, customerStore } from \"@ikas/bp-storefront\";\n\n// Takes the ADDRESS OBJECT, not an id:\nawait deleteCustomerAddress(customerStore, address);\n```\n\n### Key rules\n\n1. Render from `addressForm.addressFormat`, never from a hardcoded field list.\n2. Wrap the component in `observer()` unless it is the root export -- otherwise it never re-renders.\n3. `title` is rendered outside the matrix; it is not an `AddressFormItem`.\n4. Filter each row by `form[key] != null` and drop rows that end up empty.\n5. Always fall back to a text input when `isFreeText` is set or options are empty.\n6. Gate the form on `initAddressForm()` resolving; show a skeleton meanwhile.\n7. Use `getEmptyAddressForm(store)` to create and `getIkasCustomerAddressForm(address)` to edit.\n8. Let `submitAddressForm()` validate; read `hasError`/`message` off the fields.",
352
+ "tags": [
353
+ "address",
354
+ "addressFormat",
355
+ "address-form",
356
+ "form-layout",
357
+ "country",
358
+ "state",
359
+ "city",
360
+ "district",
361
+ "region",
362
+ "postal-code",
363
+ "free-text",
364
+ "checkout",
365
+ "account",
366
+ "renderFormField"
297
367
  ]
298
368
  },
299
369
  "account-patterns": {
300
370
  "title": "Account & Dashboard Patterns",
301
371
  "description": "Orders list, order detail, address management, favorites, and account info forms",
302
- "content": "## Account & Dashboard Patterns\n\nAccount sections typically use a tab-based layout with orders, addresses, favorites, and account info.\n\n### Orders List\n\n```tsx\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n} from \"@ikas/bp-storefront\";\n\n// Fetch orders on mount\nuseEffect(() => {\n getOrders(customerStore);\n}, []);\n\nconst orders = customerStore.orders ?? [];\n\n{orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <span>Order #{order.orderNumber}</span>\n <span>{getIkasOrderFormattedOrderedAt(order)}</span>\n <span>{getIkasOrderTotalItemCount(order)} items</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n <span>{getIkasOrderPackageStatusTranslation(order)}</span>\n </a>\n))}\n```\n\n### Order Detail\n\n```tsx\nimport {\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// order.orderLineItems contains line items\n{order.orderLineItems.map((item) => {\n const image = item.variant\n ? getIkasOrderLineVariantMainImage(item.variant)\n : null;\n return (\n <div key={item.id}>\n {image && <img src={getDefaultSrc(image)} />}\n <span>{item.variant?.name}</span>\n <span>Qty: {item.quantity}</span>\n </div>\n );\n})}\n```\n\n### Address Management\n\n```tsx\nimport {\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteAddress,\n} from \"@ikas/bp-storefront\";\n\nconst addresses = customerStore.customer?.addresses ?? [];\n\n// Create new address\nconst addressForm = getIkasCustomerAddressForm(customerStore);\ninitAddressForm(addressForm);\n// ... set fields ...\nawait submitAddressForm(addressForm);\n\n// Delete address\nawait deleteAddress(customerStore, addressId);\n```\n\n### Favorites\n\n```tsx\nimport {\n getFavoriteProducts,\n isFavoriteIkasProduct,\n removeIkasProductFromFavorites,\n} from \"@ikas/bp-storefront\";\n\n// Fetch favorites\nuseEffect(() => {\n getFavoriteProducts(customerStore);\n}, []);\n\nconst favorites = customerStore.favoriteProducts ?? [];\n```\n\n### Account Info Form\n\n```tsx\nimport {\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nconst accountForm = getAccountInfoForm(customerStore);\n\nuseEffect(() => {\n initAccountInfoForm(accountForm);\n}, []);\n\nconst handleSave = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n};\n```\n\n### Tab Navigation Pattern\n\nProduction themes use a tab-based layout:\n\n```tsx\nconst [activeTab, setActiveTab] = useState(\"orders\");\n\nconst tabs = [\n { id: \"orders\", label: \"Orders\" },\n { id: \"addresses\", label: \"Addresses\" },\n { id: \"favorites\", label: \"Favorites\" },\n { id: \"account\", label: \"Account Info\" },\n];\n\n<div className=\"account-tabs\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n className={activeTab === tab.id ? \"active\" : \"\"}\n onClick={() => setActiveTab(tab.id)}\n >\n {tab.label}\n </button>\n ))}\n</div>\n\n{activeTab === \"orders\" && <OrdersList />}\n{activeTab === \"addresses\" && <AddressList />}\n{activeTab === \"favorites\" && <FavoritesList />}\n{activeTab === \"account\" && <AccountInfoForm />}\n```",
372
+ "content": "## Account & Dashboard Patterns\n\nAccount sections typically use a tab-based layout with orders, addresses, favorites, and account info.\n\n> **`customerStore` has exactly one public field: `customer`.** Everything else on the store is\n> underscore-prefixed internal state. Order and favorite lists are **not** fields on the store —\n> they come from the `Promise` that `getOrders()` / `getFavoriteProducts()` return. Reading\n> `customerStore.orders` or `customerStore.favoriteProducts` is a TypeScript error (TS2339 /\n> TS2551), not a silent empty list.\n>\n> This is the one place where copying data into `useState` is correct. The\n> \"don't mirror store data in `useState`\" rule in `async-data-patterns` §5 is about\n> store-backed observable fields; orders and favorites have no such field to observe.\n\n### Orders List\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n IkasOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n} from \"@ikas/bp-storefront\";\n\n// getOrders() RESOLVES with the orders — it writes nothing to the store.\n// Keep the result in local state; there is no `customerStore.orders`.\nconst [orders, setOrders] = useState<IkasOrder[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getOrders(customerStore)\n .then((result) => {\n if (!cancelled) setOrders(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n\n{orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <span>Order #{order.orderNumber}</span>\n <span>{getIkasOrderFormattedOrderedAt(order)}</span>\n <span>{getIkasOrderTotalItemCount(order)} items</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n <span>{getIkasOrderPackageStatusTranslation(order)}</span>\n </a>\n))}\n```\n\n### Order Detail\n\n```tsx\nimport {\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// order.orderLineItems contains line items\n{order.orderLineItems.map((item) => {\n const image = item.variant\n ? getIkasOrderLineVariantMainImage(item.variant)\n : null;\n return (\n <div key={item.id}>\n {image && <img src={getDefaultSrc(image)} />}\n <span>{item.variant?.name}</span>\n <span>Qty: {item.quantity}</span>\n </div>\n );\n})}\n```\n\n### Address Management\n\nAddress forms are country-driven: the field set and the row layout come from\n`addressForm.addressFormat`, not from a list you write. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building one.\n\n```tsx\nimport {\n getEmptyAddressForm,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n customerStore,\n} from \"@ikas/bp-storefront\";\n\nconst addresses = customerStore.customer?.addresses ?? [];\n\n// Create: getEmptyAddressForm takes the STORE.\n// Edit: getIkasCustomerAddressForm takes the ADDRESS.\nconst addressForm = address ? getIkasCustomerAddressForm(address) : getEmptyAddressForm(customerStore);\n\n// init is async and loads the country format + location lists; gate render on it.\nawait initAddressForm(addressForm, address);\n\n// Render rows from addressForm.addressFormat, set values via setAddressForm*(addressForm, value),\n// then submit (it validates and returns a boolean):\nconst ok = await submitAddressForm(addressForm);\n\n// Delete takes the ADDRESS OBJECT, not an id:\nawait deleteCustomerAddress(customerStore, address);\n```\n\n### Favorites\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\n// Same rule as orders: getFavoriteProducts() resolves with the products.\n// There is no `customerStore.favoriteProducts` field.\nconst [favorites, setFavorites] = useState<IkasProduct[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getFavoriteProducts(customerStore)\n .then((result) => {\n if (!cancelled) setFavorites(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n```\n\nThe favorites **list** is a fetched snapshot, but the favorite **flag** is store-backed and\nreactive — `isFavoriteIkasProduct(product)` reads the store synchronously, so heart icons inside\nan `observer()` sub-component update on their own:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n} from \"@ikas/bp-storefront\";\n\nconst FavoriteButton = observer(function FavoriteButton({ product }: { product: IkasProduct }) {\n const isFavorite = isFavoriteIkasProduct(product);\n return (\n <button\n onClick={() =>\n isFavorite\n ? removeIkasProductFromFavorites(product)\n : addIkasProductToFavorites(product)\n }\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n );\n});\n```\n\nBecause the list is a snapshot, removing a favorite does **not** re-render it. Drop the item from\nyour local array as well:\n\n```tsx\nonFavoriteRemove={() => setFavorites((prev) => prev.filter((f) => f.id !== product.id))}\n```\n\nNote: `getFavoriteProducts` fetches at most 100 products in one call.\n\n### Account Info Form\n\n```tsx\nimport {\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nconst accountForm = getAccountInfoForm(customerStore);\n\nuseEffect(() => {\n initAccountInfoForm(accountForm);\n}, []);\n\nconst handleSave = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n};\n```\n\n### Delete Account (Deactivate Customer)\n\nAccount deletion is a form like any other — the customer confirms with their password. The form\ndoes **not** clear the session, so call `logout(customerStore)` yourself once it succeeds.\n\n```tsx\nimport {\n customerStore,\n getDeactivateCustomerForm,\n initDeactivateCustomerForm,\n setDeactivateCustomerFormPassword,\n submitDeactivateCustomerForm,\n logout,\n Router,\n} from \"@ikas/bp-storefront\";\n\nconst deactivateForm = getDeactivateCustomerForm(customerStore);\n\nuseEffect(() => {\n initDeactivateCustomerForm(deactivateForm);\n}, []);\n\nconst handleDelete = async (e: Event) => {\n e.preventDefault();\n const success = await submitDeactivateCustomerForm(deactivateForm);\n if (success) {\n await logout(customerStore);\n Router.navigateToPage(\"INDEX\");\n }\n};\n\n<form onSubmit={handleDelete}>\n {deactivateForm.isFailure && <div>{deactivateForm.responseMessage}</div>}\n\n <input\n type=\"password\"\n value={deactivateForm.password.value}\n onInput={(e) => setDeactivateCustomerFormPassword(deactivateForm, e.target.value)}\n />\n {deactivateForm.password.hasError && <span>{deactivateForm.password.message}</span>}\n\n <button type=\"submit\" disabled={deactivateForm.isSubmitting}>\n {deactivateForm.isSubmitting ? \"Deleting...\" : \"Delete Account\"}\n </button>\n</form>\n```\n\n`submit` already turns the API error for an existing deletion request into a translated\n`responseMessage` — render `responseMessage` rather than inspecting the API response yourself.\nDeletion is irreversible, so gate the form behind a confirmation step in your own UI.\n\n### Tab Navigation Pattern\n\nProduction themes use a tab-based layout:\n\n```tsx\nconst [activeTab, setActiveTab] = useState(\"orders\");\n\nconst tabs = [\n { id: \"orders\", label: \"Orders\" },\n { id: \"addresses\", label: \"Addresses\" },\n { id: \"favorites\", label: \"Favorites\" },\n { id: \"account\", label: \"Account Info\" },\n];\n\n<div className=\"account-tabs\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n className={activeTab === tab.id ? \"active\" : \"\"}\n onClick={() => setActiveTab(tab.id)}\n >\n {tab.label}\n </button>\n ))}\n</div>\n\n{activeTab === \"orders\" && <OrdersList />}\n{activeTab === \"addresses\" && <AddressList />}\n{activeTab === \"favorites\" && <FavoritesList />}\n{activeTab === \"account\" && <AccountInfoForm />}\n```",
303
373
  "tags": [
304
374
  "account",
305
375
  "orders",
@@ -307,7 +377,10 @@
307
377
  "favorites",
308
378
  "dashboard",
309
379
  "customer",
310
- "tabs"
380
+ "tabs",
381
+ "deactivate",
382
+ "delete account",
383
+ "password"
311
384
  ]
312
385
  },
313
386
  "header-footer-patterns": {
@@ -334,14 +407,7 @@
334
407
  "title": "Customer Review Patterns",
335
408
  "description": "Review display, star ratings, review submission form, and login-required checks",
336
409
  "content": "## Customer Review Patterns\n\nProduct reviews allow customers to rate and comment on products. The pattern includes displaying existing reviews and a submission form.\n\n### Displaying Reviews\n\n```tsx\nimport {\n getProductCustomerReviews,\n getIkasCustomerReviewFormattedDate,\n} from \"@ikas/bp-storefront\";\n\n// In your component (product prop):\nconst reviews = getProductCustomerReviews(product) ?? [];\n\n{reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= review.star ? \"filled\" : \"empty\"}>★</span>\n ))}\n </div>\n <h4>{review.title}</h4>\n <p>{review.comment}</p>\n <span className=\"review-date\">{getIkasCustomerReviewFormattedDate(review)}</span>\n <span className=\"review-author\">{review.customerName}</span>\n </div>\n))}\n```\n\n### Review Submission Form\n\n```tsx\nimport {\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\n\nconst reviewForm = getIkasProductCustomerReviewForm(product);\nconst loginRequired = isCustomerReviewLoginRequired() as unknown as boolean;\nconst isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n// If login is required and user is not logged in\nif (loginRequired && !isLoggedIn) {\n return (\n <div>\n <p>Please log in to write a review.</p>\n <button onClick={() => Router.navigateToPage(\"LOGIN\")}>Log In</button>\n </div>\n );\n}\n\n// Star rating input\nconst [hoverStar, setHoverStar] = useState(0);\n\n<div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n className={star <= (hoverStar || reviewForm.star.value) ? \"filled\" : \"empty\"}\n onMouseEnter={() => setHoverStar(star)}\n onMouseLeave={() => setHoverStar(0)}\n onClick={() => setCustomerReviewFormStar(reviewForm, star)}\n >\n ★\n </button>\n ))}\n</div>\n\n// Title and comment inputs\n<input\n value={reviewForm.title.value}\n onInput={(e) => setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)}\n/>\n<textarea\n value={reviewForm.comment.value}\n onInput={(e) => setCustomerReviewFormComment(reviewForm, (e.target as HTMLTextAreaElement).value)}\n/>\n\n// Submit\nconst handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) {\n // Review submitted successfully\n }\n};\n```\n\n### Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `getProductCustomerReviews(product)` | Get reviews for a product |\n| `getIkasProductCustomerReviewForm(product)` | Get the review form model |\n| `setCustomerReviewFormTitle(form, value)` | Set review title |\n| `setCustomerReviewFormStar(form, value)` | Set star rating (1-5) |\n| `setCustomerReviewFormComment(form, value)` | Set review comment |\n| `submitCustomerReviewForm(form)` | Submit the review |\n| `isCustomerReviewLoginRequired()` | Check if login is needed to review |\n| `getIkasCustomerReviewFormattedDate(review)` | Format review date |",
337
- "tags": [
338
- "reviews",
339
- "ratings",
340
- "stars",
341
- "feedback",
342
- "customer",
343
- "form"
344
- ]
410
+ "tags": ["reviews", "ratings", "stars", "feedback", "customer", "form"]
345
411
  },
346
412
  "slider-overlay-patterns": {
347
413
  "title": "Slider & Overlay Patterns (Serel Reference)",
@@ -366,14 +432,7 @@
366
432
  "title": "Blog Patterns",
367
433
  "description": "Blog list display, blog post detail, pagination, date formatting, and category filtering",
368
434
  "content": "## Blog Patterns\n\nBlog sections display articles with pagination and optional category filtering.\n\n### Blog List Prop Setup\n\nIn `ikas.config.json`, use the `BLOG_LIST` prop type or a dedicated blog list config:\n\n```json\n{\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_LIST\"\n}\n```\n\n### Blog List Display\n\n```tsx\nimport {\n IkasBlogList,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// In your component:\nconst blogs = blogList?.data ?? [];\n\n{blogs.map((blog) => (\n <a key={blog.id} href={getIkasBlogHref(blog)} className=\"blog-card\">\n {blog.image && (\n <img src={getDefaultSrc(blog.image)} alt={blog.title} />\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-date\">{getIkasBlogFormattedDate(blog)}</span>\n <h3>{blog.title}</h3>\n <p>{blog.summary}</p>\n </div>\n </a>\n))}\n```\n\n### Blog Pagination\n\n```tsx\nconst hasNext = hasBlogListNextPage(blogList);\nconst hasPrev = hasBlogListPrevPage(blogList);\n\n<div className=\"blog-pagination\">\n {hasPrev && (\n <button onClick={() => getBlogListPrevPage(blogList)}>Previous</button>\n )}\n {hasNext && (\n <button onClick={() => getBlogListNextPage(blogList)}>Next</button>\n )}\n</div>\n```\n\n### Blog Post Detail\n\nFor a blog post detail page, use the `BLOG` prop type:\n\n```json\n{\n \"name\": \"blogPost\",\n \"displayName\": \"Blog Post\",\n \"type\": \"BLOG\"\n}\n```\n\n```tsx\ninterface Props {\n blogPost: IkasBlog;\n}\n\nfunction BlogPostDetail({ blogPost }: Props) {\n if (!blogPost) return null;\n\n return (\n <article className=\"blog-post\">\n {blogPost.image && (\n <img src={getDefaultSrc(blogPost.image)} alt={blogPost.title} />\n )}\n <h1>{blogPost.title}</h1>\n <span className=\"date\">{getIkasBlogFormattedDate(blogPost)}</span>\n <div\n className=\"blog-content\"\n dangerouslySetInnerHTML={{ __html: blogPost.blogContent.content }}\n />\n </article>\n );\n}\n```\n\n### Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `getIkasBlogFormattedDate(blog)` | Format blog post date |\n| `getIkasBlogHref(blog)` | Get URL for a blog post |\n| `hasBlogListNextPage(list)` | Check if next page exists |\n| `getBlogListNextPage(list)` | Fetch next page (mutates list) |\n| `hasBlogListPrevPage(list)` | Check if previous page exists |\n| `getBlogListPrevPage(list)` | Fetch previous page (mutates list) |",
369
- "tags": [
370
- "blog",
371
- "article",
372
- "posts",
373
- "pagination",
374
- "date",
375
- "content"
376
- ]
435
+ "tags": ["blog", "article", "posts", "pagination", "date", "content"]
377
436
  },
378
437
  "navigation-patterns": {
379
438
  "title": "Navigation & Routing Patterns",
@@ -410,7 +469,7 @@
410
469
  "prop-groups": {
411
470
  "title": "Prop Groups — Organizing Component Properties",
412
471
  "description": "How to organize props into collapsible groups in the editor sidebar",
413
- "content": "## Prop Groups\n\nProp groups organize a component's props into collapsible containers in the editor sidebar, making complex components easier to configure.\n\n### When to Use\n- Components with 5+ props\n- All section templates with multiple categories (content vs style vs data)\n- Any component where related props should be visually grouped\n\n### Config Format\n\nDefine `propGroups` on the component and assign props via `groupId`:\n\n```json\n{\n \"name\": \"HeroBanner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"type\": \"TEXT\", \"groupId\": \"content\", ... },\n { \"name\": \"subtitle\", \"type\": \"TEXT\", \"groupId\": \"content\", ... },\n { \"name\": \"bgColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\", ... },\n { \"name\": \"textColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\", ... },\n { \"name\": \"showBadge\", \"type\": \"BOOLEAN\", ... }\n ],\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\", \"description\": \"Text and media\" },\n {\n \"id\": \"style\", \"name\": \"Style\",\n \"children\": [\n { \"id\": \"style.colors\", \"name\": \"Colors\" }\n ]\n }\n ]\n}\n```\n\n### Rules\n- `propGroups` and `groupId` are optional (backward-compatible)\n- Props without `groupId` appear ungrouped at root level\n- Groups can nest 1 level deep via `children`\n- Group IDs must be unique within a component (use kebab-case)\n\n### CLI Commands\n```bash\n# Create a group\nnpx ikas-component config add-prop-group --component \"Name\" --id content --name \"Content\"\n\n# Create a nested group\nnpx ikas-component config add-prop-group --component \"Name\" --id colors --name \"Colors\" --parent style\n\n# Add a prop to a group\nnpx ikas-component config add-prop --component \"Name\" --name title --displayName \"Title\" --type TEXT --group content\n\n# Move a prop to a different group\nnpx ikas-component config update-prop --component \"Name\" --prop title --group colors\n\n# Remove a group (props become ungrouped)\nnpx ikas-component config remove-prop-group --component \"Name\" --id content\n```\n\n### Common Patterns\n- Simple section: \"Content\" + \"Appearance\" groups\n- Complex section: \"Content\" + \"Style\" → (\"Colors\", \"Typography\") nested groups\n- Data-heavy section: \"Content\" + \"Data\" + \"Appearance\" groups",
472
+ "content": "## Prop Groups\n\nProp groups organize a component's props into collapsible containers in the editor sidebar, making complex components easier to configure.\n\n### When to Use\n- Components with 5+ props\n- All section templates with multiple categories (content vs style vs data)\n- Any component where related props should be visually grouped\n\n### Config Format\n\nDefine `propGroups` on the component and assign props via `groupId`:\n\n```json\n{\n \"name\": \"HeroBanner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"type\": \"TEXT\", \"groupId\": \"content\", ... },\n { \"name\": \"subtitle\", \"type\": \"TEXT\", \"groupId\": \"content\", ... },\n { \"name\": \"bgColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\", ... },\n { \"name\": \"textColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\", ... },\n { \"name\": \"showBadge\", \"type\": \"BOOLEAN\", ... }\n ],\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\", \"description\": \"Text and media\" },\n {\n \"id\": \"style\", \"name\": \"Style\",\n \"children\": [\n { \"id\": \"style.colors\", \"name\": \"Colors\" }\n ]\n }\n ]\n}\n```\n\n### Rules\n- `propGroups` and `groupId` are optional (backward-compatible)\n- Props without `groupId` appear ungrouped at root level\n- Groups can nest 1 level deep via `children`\n- Group IDs must be unique within a component (use kebab-case)\n- A group can be gated on a BOOLEAN/ENUM prop via `requiredProp` — it and everything under it are drawn only while that prop satisfies the condition. The governing prop must live OUTSIDE the group it gates. See `get_framework_guide(\"conditional-visibility\")`\n\n### CLI Commands\n```bash\n# Create a group\nnpx ikas-component config add-prop-group --component \"Name\" --id content --name \"Content\"\n\n# Create a nested group\nnpx ikas-component config add-prop-group --component \"Name\" --id colors --name \"Colors\" --parent style\n\n# Add a prop to a group\nnpx ikas-component config add-prop --component \"Name\" --name title --displayName \"Title\" --type TEXT --group content\n\n# Move a prop to a different group\nnpx ikas-component config update-prop --component \"Name\" --prop title --group colors\n\n# Remove a group (props become ungrouped)\nnpx ikas-component config remove-prop-group --component \"Name\" --id content\n\n# Show a group only while a BOOLEAN prop is on (see the conditional-visibility guide)\nnpx ikas-component config update-prop-group --component \"Name\" --id badge --requiredProp showBadge\n```\n\n### Common Patterns\n- Simple section: \"Content\" + \"Appearance\" groups\n- Complex section: \"Content\" + \"Style\" → (\"Colors\", \"Typography\") nested groups\n- Data-heavy section: \"Content\" + \"Data\" + \"Appearance\" groups",
414
473
  "tags": [
415
474
  "props",
416
475
  "groups",
@@ -418,7 +477,35 @@
418
477
  "sidebar",
419
478
  "config",
420
479
  "propGroups",
421
- "groupId"
480
+ "groupId",
481
+ "requiredProp",
482
+ "conditional",
483
+ "visibility"
484
+ ]
485
+ },
486
+ "conditional-visibility": {
487
+ "title": "Conditional Visibility — Gating Props and Prop Groups",
488
+ "description": "Show a prop or a prop group in the editor panel only while another BOOLEAN/ENUM prop on the same component satisfies a condition (requiredProp / requiredPropValue / requiredEnumOptionValue)",
489
+ "content": "## Conditional Visibility\n\nA prop — or a whole prop group — can name another prop that **gates** it. The control is drawn in\nthe editor's settings panel only while the named prop satisfies the condition, so a merchant sees\nthe settings that apply to the layout they picked and nothing else.\n\nTwo consequences, and they never disagree:\n\n- the control is not drawn while the condition fails, **and**\n- `required` is not enforced on a prop whose control is not drawn — a merchant cannot fill a field\n that is not on screen. This is why a `required` prop *may* be gated.\n\nIt is presentation only: a hidden prop's value is still stored, still passed to your component and\nstill published, and turning the toggle back on brings the control back with what it held. **Your\ncomponent still receives the prop** — if a value must not affect the render while the toggle is off,\nbranch on the toggle in the component too:\n\n```tsx\nexport function HeroBanner({ showBadge, badgeText }: Props) {\n return <section>{showBadge && <span className=\"badge\">{badgeText}</span>}</section>;\n}\n```\n\n### Config Format\n\nThe governing prop is `BOOLEAN` or `ENUM` and lives on the **same component**.\n\n- **BOOLEAN gate** — shown while the prop holds `requiredPropValue` (defaults to `true`)\n- **ENUM gate** — shown while the prop's value equals `requiredEnumOptionValue`\n\n```json\n{\n \"name\": \"HeroBanner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"showBadge\", \"type\": \"BOOLEAN\", \"displayName\": \"Show Badge\", \"defaultValue\": false },\n { \"name\": \"layout\", \"type\": \"ENUM\", \"displayName\": \"Layout\", \"enumTypeId\": \"aBcDeFgHiJ\" },\n\n { \"name\": \"badgeText\", \"type\": \"TEXT\", \"displayName\": \"Badge Text\", \"requiredProp\": \"showBadge\" },\n { \"name\": \"emptyNote\", \"type\": \"TEXT\", \"displayName\": \"Empty Note\", \"requiredProp\": \"showBadge\", \"requiredPropValue\": false },\n { \"name\": \"columns\", \"type\": \"NUMBER\", \"displayName\": \"Columns\", \"requiredProp\": \"layout\", \"requiredEnumOptionValue\": \"grid\" }\n ],\n \"propGroups\": [\n { \"id\": \"badge\", \"name\": \"Badge\", \"requiredProp\": \"showBadge\" }\n ]\n}\n```\n\n- `requiredProp` is a prop **NAME**, not an id. Config props carry no ids (the CLI mints them from\n position), and a name survives a reorder where an index would not.\n- `requiredEnumOptionValue` is the enum option's **value**, not its label: for a custom enum created\n with `config add-enum --options '{\"Grid\":\"grid\",\"List\":\"list\"}'` the value is `grid`. Read valid\n values from `config list-types` (built-in enums), the `--options` map you authored (custom enums),\n or an ENUM prop's `options` in `get_component_props`.\n- A gated group hides **everything under it** — its props and its child groups.\n\n### Rules (hard CLI errors, not warnings)\n\n- The named prop must exist on this component and be `BOOLEAN` or `ENUM`. Nothing else can gate.\n- An ENUM gate MUST carry `requiredEnumOptionValue` (without it nothing would ever match and the\n control would never appear). A BOOLEAN gate must NOT carry one — use `requiredPropValue`.\n- `requiredPropValue` must be a real boolean; the CLI flag accepts only `true` / `false`.\n- `requiredPropValue` / `requiredEnumOptionValue` without a `requiredProp` is rejected.\n- A prop cannot gate itself, and a chain of gates cannot cycle back to it — every prop in a cycle\n would stay hidden forever, including the toggles that would let someone out of it.\n- A group cannot be gated on a prop that sits **inside** it (or inside one of its child groups):\n hiding the group would take that toggle with it and nothing could bring it back. Put the governing\n toggle ungrouped, or in another group.\n\n### CLI Commands\n\n```bash\n# Gate a prop on a toggle (default: shown while the toggle is ON)\nnpx ikas-component config add-prop --component \"HeroBanner\" --name badgeText \\\n --displayName \"Badge Text\" --type TEXT --requiredProp showBadge\n\n# Invert it — show while the toggle is OFF\nnpx ikas-component config update-prop --component \"HeroBanner\" --prop emptyNote \\\n --requiredProp showBadge --requiredPropValue false\n\n# Gate on one option of an ENUM prop\nnpx ikas-component config update-prop --component \"HeroBanner\" --prop columns \\\n --requiredProp layout --requiredEnumOptionValue grid\n\n# Gate a whole group (and everything under it)\nnpx ikas-component config add-prop-group --component \"HeroBanner\" --id badge --name \"Badge\" \\\n --requiredProp showBadge\nnpx ikas-component config update-prop-group --component \"HeroBanner\" --id badge \\\n --requiredProp showBadge\n\n# Clear a condition (every field goes together)\nnpx ikas-component config update-prop --component \"HeroBanner\" --prop badgeText --requiredProp none\nnpx ikas-component config update-prop-group --component \"HeroBanner\" --id badge --requiredProp none\n```\n\nThere is no `--requiredEnumOptionId` flag — the CLI mirrors the option value into the id for you.\n\n### Gotchas\n\n- `add-component --props '[...]'` accepts `requiredProp` on each prop, and a prop may name one\n declared **later** in the same batch (the whole batch is validated once it is parsed). Groups are\n different: `add-component` only auto-creates the groups your props reference via `groupId`, with\n no condition — gate a group afterwards with `add-prop-group` / `update-prop-group`.\n- Conditions are re-validated on every `update-prop` / `update-prop-group`, not just when you pass a\n visibility flag: renaming a governing prop or moving one into a gated group can invalidate a gate\n that was sound when it was written.\n- If the governing prop is deleted, the dangling condition is dropped when the component is\n broadcast/published and the control simply shows — the safe direction, not a substitute for\n fixing the name.\n- Do not hand-edit `types.ts` to express this; it is generated. Do not fake it by removing props\n from the config either — the value would be lost, and gating keeps it.\n\n### When to Use\n\n- an \"Enable X\" toggle plus the handful of props X needs (badge, countdown, secondary CTA)\n- a layout/variant ENUM whose options each need their own settings (columns for `grid`, autoplay\n speed for `slider`)\n- any panel that has grown past ~10 props where half never apply at once\n\nThe same feature exists for theme sections and components in the editor UI, and the built-in\naccount pages express it with their own manifest `showIf` — one rule, one evaluator, everywhere.\n",
490
+ "tags": [
491
+ "conditional",
492
+ "visibility",
493
+ "conditional-visibility",
494
+ "requiredProp",
495
+ "requiredPropValue",
496
+ "requiredEnumOptionValue",
497
+ "requiredEnumOptionId",
498
+ "gate",
499
+ "show",
500
+ "hide",
501
+ "toggle",
502
+ "dependent props",
503
+ "depends on",
504
+ "config",
505
+ "props",
506
+ "propGroups",
507
+ "BOOLEAN",
508
+ "ENUM"
422
509
  ]
423
510
  },
424
511
  "component-renderer-patterns": {
@@ -494,8 +581,8 @@
494
581
  },
495
582
  "config-advanced-features": {
496
583
  "title": "Advanced ikas.config.json Features",
497
- "description": "filteredComponentIds, privateVarMap, isHeader/isFooter, propGroups, custom enum types, typeId/enumTypeId for advanced config patterns",
498
- "content": "## Advanced ikas.config.json Features\n\nBeyond basic component and prop definitions, `ikas.config.json` supports advanced features for complex theme architectures.\n\n### filteredComponentIds — Restricting Component Slots\n\n`filteredComponentIds` on a `COMPONENT_LIST` prop limits which child components can be placed in that slot. Without it, any component can be added.\n\n```json\n{\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-navbar\",\n \"nb34u3yu-announcements\",\n \"nb34u3yu-cookie-bar\"\n ]\n}\n```\n\nThe IDs use the full component ID format: `{projectId}-{kebab-case-name}`. Check your component IDs with `npx ikas-component config list`.\n\n### privateVarMap — Section-to-Child Data Passing\n\nExposes a typed variable from a section to its child components. At runtime, the section must pass the actual data via the `map` prop on `IkasComponentRenderer`.\n\n```json\n{\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-card-product-price\"],\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"nb34u3yu-product-slider-product\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n }\n}\n```\n\n- `product` = variable name children receive\n- `id` = unique identifier (project-section-variable format)\n- `typeId` = model type reference (`@ikas/bp-storefront-models-TypeName`)\n\nSee `get_framework_guide(\"private-var-map\")` for full details.\n\n### isHeader / isFooter — Global Sections\n\nMark a section to appear on every page:\n\n```json\n{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"isHeader\": true,\n \"props\": [...]\n}\n```\n\nRules:\n- Only one component can have `isHeader: true`\n- Only one component can have `isFooter: true`\n- Only applies to `type: \"section\"` components\n- The editor automatically adds these to all pages\n\nCLI:\n```bash\nnpx ikas-component config add-component --name \"Header\" --type section --isHeader --props '[...]'\nnpx ikas-component config add-component --name \"Footer\" --type section --isFooter --props '[...]'\n```\n\n### propGroups — Organizing Props\n\nGroup props into collapsible sections in the editor sidebar:\n\n```json\n{\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\" },\n {\n \"id\": \"style\", \"name\": \"Style\",\n \"children\": [\n { \"id\": \"style.colors\", \"name\": \"Colors\" }\n ]\n }\n ],\n \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\", \"groupId\": \"content\" },\n { \"name\": \"bgColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\" }\n ]\n}\n```\n\nCLI:\n```bash\nnpx ikas-component config add-prop-group --component \"MySection\" --id content --name \"Content\"\nnpx ikas-component config add-prop-group --component \"MySection\" --id colors --name \"Colors\" --parent style\nnpx ikas-component config update-prop --component \"MySection\" --prop title --group content\n```\n\nSee `get_framework_guide(\"prop-groups\")` for full details.\n\n### TYPE Props — Structured Style Types\n\nThe `TYPE` prop type uses `typeId` to reference structured style types (padding, margin, border-radius, etc.):\n\n```json\n{\n \"name\": \"spacing\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-PaddingStyleType\"\n}\n```\n\nAppend `_array` for array types: `\"@ikas/bp-storefront-models-MarginStyleType_array\"`\n\nDiscover available types:\n```bash\nnpx ikas-component config list-types # all types\nnpx ikas-component config list-types --component-type section # section-allowed types only\n```\n\n### ENUM Props — Enum Style Types\n\nThe `ENUM` prop type uses `enumTypeId` for dropdown selectors:\n\n```json\n{\n \"name\": \"direction\",\n \"type\": \"ENUM\",\n \"enumTypeId\": \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n}\n```\n\nCommon enum types: FlexDirectionStyleType, JustifyContentStyleType, AlignItemsStyleType, TextAlignStyleType, ObjectFitStyleType.\n\nCLI:\n```bash\nnpx ikas-component config add-prop --component MyComp --name direction --displayName Direction --type ENUM --enumTypeId \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n```\n\n### CLI Config Commands Reference\n\n| Command | Purpose |\n|---------|--------|\n| `config add-component --name X --type section --props '[...]'` | Create component with props |\n| `config add-prop --component X --name y --type TYPE --typeId Z` | Add structured type prop |\n| `config add-prop --component X --name y --type ENUM --enumTypeId Z` | Add enum prop |\n| `config add-prop-group --component X --id g --name \"Group\"` | Create prop group |\n| `config update-prop --component X --prop y --group g` | Assign prop to group |\n| `config list-types` | List available TYPE and ENUM types |\n| `config list` | List all components and props |",
584
+ "description": "filteredComponentIds, privateVarMap, isHeader/isFooter, propGroups, requiredProp conditional visibility, custom enum types, typeId/enumTypeId for advanced config patterns",
585
+ "content": "## Advanced ikas.config.json Features\n\nBeyond basic component and prop definitions, `ikas.config.json` supports advanced features for complex theme architectures.\n\n### filteredComponentIds — Restricting Component Slots\n\n`filteredComponentIds` on a `COMPONENT_LIST` prop limits which child components can be placed in that slot. Without it, any component can be added.\n\n```json\n{\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-navbar\",\n \"nb34u3yu-announcements\",\n \"nb34u3yu-cookie-bar\"\n ]\n}\n```\n\nThe IDs use the full component ID format: `{projectId}-{kebab-case-name}`. Check your component IDs with `npx ikas-component config list`.\n\n### privateVarMap — Section-to-Child Data Passing\n\nExposes a typed variable from a section to its child components. At runtime, the section must pass the actual data via the `map` prop on `IkasComponentRenderer`.\n\n```json\n{\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-card-product-price\"],\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"nb34u3yu-product-slider-product\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n }\n}\n```\n\n- `product` = variable name children receive\n- `id` = unique identifier (project-section-variable format)\n- `typeId` = model type reference (`@ikas/bp-storefront-models-TypeName`)\n\nSee `get_framework_guide(\"private-var-map\")` for full details.\n\n### isHeader / isFooter — Global Sections\n\nMark a section to appear on every page:\n\n```json\n{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"isHeader\": true,\n \"props\": [...]\n}\n```\n\nRules:\n- Only one component can have `isHeader: true`\n- Only one component can have `isFooter: true`\n- Only applies to `type: \"section\"` components\n- The editor automatically adds these to all pages\n- Their PROP VALUES are shared theme-wide, not per page: a global section keeps ONE record of values, stored on the INDEX (Home) page's placement. Reading or writing them from any page (`get_section_values` / `update_section_prop` / `update_page_sections`) resolves to that single record, and those tools return a `common` block saying so. Two consequences when filling content: a change made from any page changes the header/footer on ALL pages, and the props are normally already filled with the merchant's content — so read the current values first and change only what was asked instead of blind-filling props that merely look empty.\n\nCLI:\n```bash\nnpx ikas-component config add-component --name \"Header\" --type section --isHeader --props '[...]'\nnpx ikas-component config add-component --name \"Footer\" --type section --isFooter --props '[...]'\n```\n\n### propGroups — Organizing Props\n\nGroup props into collapsible sections in the editor sidebar:\n\n```json\n{\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\" },\n {\n \"id\": \"style\", \"name\": \"Style\",\n \"children\": [\n { \"id\": \"style.colors\", \"name\": \"Colors\" }\n ]\n }\n ],\n \"props\": [\n { \"name\": \"title\", \"type\": \"TEXT\", \"groupId\": \"content\" },\n { \"name\": \"bgColor\", \"type\": \"COLOR\", \"groupId\": \"style.colors\" }\n ]\n}\n```\n\nCLI:\n```bash\nnpx ikas-component config add-prop-group --component \"MySection\" --id content --name \"Content\"\nnpx ikas-component config add-prop-group --component \"MySection\" --id colors --name \"Colors\" --parent style\nnpx ikas-component config update-prop --component \"MySection\" --prop title --group content\n```\n\nSee `get_framework_guide(\"prop-groups\")` for full details.\n\n### requiredProp — Conditional Visibility\n\nShow a prop, or a whole prop group, only while another `BOOLEAN` or `ENUM` prop of the same component satisfies a condition. The editor panel hides the control, and a `required` prop that is hidden this way stops blocking a publish; the value itself is still stored, passed to the component and published.\n\n```json\n{\n \"props\": [\n { \"name\": \"showBadge\", \"type\": \"BOOLEAN\", \"defaultValue\": false },\n { \"name\": \"badgeText\", \"type\": \"TEXT\", \"requiredProp\": \"showBadge\" },\n { \"name\": \"layout\", \"type\": \"ENUM\", \"enumTypeId\": \"aBcDeFgHiJ\" },\n { \"name\": \"columns\", \"type\": \"NUMBER\", \"requiredProp\": \"layout\", \"requiredEnumOptionValue\": \"grid\" }\n ],\n \"propGroups\": [\n { \"id\": \"badge\", \"name\": \"Badge\", \"requiredProp\": \"showBadge\" }\n ]\n}\n```\n\n`requiredProp` is a prop NAME. A BOOLEAN gate takes `requiredPropValue` (default `true`), an ENUM gate takes `requiredEnumOptionValue` (the option's value). The CLI rejects a gate on a missing or non-BOOLEAN/ENUM prop, a self-reference or cycle, and a group gated by a prop inside it.\n\nCLI:\n```bash\nnpx ikas-component config add-prop --component \"MySection\" --name badgeText --displayName \"Badge Text\" --type TEXT --requiredProp showBadge\nnpx ikas-component config update-prop --component \"MySection\" --prop columns --requiredProp layout --requiredEnumOptionValue grid\nnpx ikas-component config update-prop-group --component \"MySection\" --id badge --requiredProp showBadge\n```\n\nSee `get_framework_guide(\"conditional-visibility\")` for the full contract and gotchas.\n\n### TYPE Props — Structured Style Types\n\nThe `TYPE` prop type uses `typeId` to reference structured style types (padding, margin, border-radius, etc.):\n\n```json\n{\n \"name\": \"spacing\",\n \"type\": \"TYPE\",\n \"typeId\": \"@ikas/bp-storefront-models-PaddingStyleType\"\n}\n```\n\nAppend `_array` for array types: `\"@ikas/bp-storefront-models-MarginStyleType_array\"`\n\nDiscover available types:\n```bash\nnpx ikas-component config list-types # all types\nnpx ikas-component config list-types --component-type section # section-allowed types only\n```\n\n### ENUM Props — Enum Style Types\n\nThe `ENUM` prop type uses `enumTypeId` for dropdown selectors:\n\n```json\n{\n \"name\": \"direction\",\n \"type\": \"ENUM\",\n \"enumTypeId\": \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n}\n```\n\nCommon enum types: FlexDirectionStyleType, JustifyContentStyleType, AlignItemsStyleType, TextAlignStyleType, ObjectFitStyleType.\n\nCLI:\n```bash\nnpx ikas-component config add-prop --component MyComp --name direction --displayName Direction --type ENUM --enumTypeId \"@ikas/bp-storefront-models-FlexDirectionStyleType\"\n```\n\n### CLI Config Commands Reference\n\n| Command | Purpose |\n|---------|--------|\n| `config add-component --name X --type section --props '[...]'` | Create component with props |\n| `config add-prop --component X --name y --type TYPE --typeId Z` | Add structured type prop |\n| `config add-prop --component X --name y --type ENUM --enumTypeId Z` | Add enum prop |\n| `config add-prop-group --component X --id g --name \"Group\"` | Create prop group |\n| `config update-prop --component X --prop y --group g` | Assign prop to group |\n| `config add-prop --component X --name y --type TEXT --requiredProp toggleName` | Add a prop gated on a BOOLEAN prop |\n| `config update-prop --component X --prop y --requiredProp enumName --requiredEnumOptionValue grid` | Gate a prop on one ENUM option |\n| `config update-prop-group --component X --id g --requiredProp toggleName` | Gate a whole prop group |\n| `config list-types` | List available TYPE and ENUM types |\n| `config list` | List all components and props |",
499
586
  "tags": [
500
587
  "config",
501
588
  "filteredComponentIds",
@@ -508,7 +595,12 @@
508
595
  "TYPE",
509
596
  "ENUM",
510
597
  "advanced",
511
- "CLI"
598
+ "CLI",
599
+ "requiredProp",
600
+ "requiredPropValue",
601
+ "requiredEnumOptionValue",
602
+ "conditional",
603
+ "visibility"
512
604
  ]
513
605
  },
514
606
  "custom-enums": {
@@ -531,8 +623,8 @@
531
623
  },
532
624
  "image-handling": {
533
625
  "title": "Image Handling",
534
- "description": "How to render images from IkasImage, IkasProduct variants, and IkasProductImage wrappers. Covers the canonical variant-to-image chain, all-variant-images mapping, non-product images, background images, and responsive srcsets.",
535
- "content": "## Image Handling\n\nThe canonical recipe for rendering an image. The product-variant case is 4 hops because each step carries metadata the next doesn't (variant selection → that variant's image set → metadata wrapper → CDN-ready image):\n\n```tsx\nimport {\n getSelectedProductVariant,\n getProductVariantMainImage,\n getDefaultSrc,\n getSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nconst variant = getSelectedProductVariant(product); // IkasProductVariant | undefined\nconst productImage = variant && getProductVariantMainImage(variant); // IkasProductImage | undefined\nconst image = productImage?.image; // IkasImage | undefined — what CDN helpers need\n\nif (image) {\n return (\n <>\n <img src={getDefaultSrc(image)} alt={product.name} /> {/* default size */}\n <img src={getSrc(image, { width: 400 })} alt=\"\" /> {/* explicit width */}\n <img srcSet={createMediaSrcset(image)} alt=\"\" /> {/* responsive srcset */}\n </>\n );\n}\n```\n\n## All Variant Images (gallery)\n\nMap over `variant.images` and unwrap each `.image`. `IkasProductImage` carries metadata like `order` and `isMain`; the actual `IkasImage` (what the CDN helpers need) is the `.image` property on each.\n\n```tsx\nconst images: IkasImage[] = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n## Non-Product Images (logo, banner, IMAGE prop)\n\nThese skip the variant hop — they're already `IkasImage`:\n\n```tsx\n{props.logo && <img src={getDefaultSrc(props.logo)} alt=\"\" />}\n```\n\n## Background Images\n\n```tsx\n<div style={{ backgroundImage: image ? `url(${getDefaultSrc(image)})` : \"none\" }} />\n```\n\n## See Also\n\n- `get_framework_guide(\"common-pitfalls\")` §19 — full property-rename catalog (`selectedVariant`, `mainImage`, `href`, `checkoutUrl`)\n- `get_section_template(\"image-handling\")` — full Product Detail section with gallery + zoom + srcset\n- `get_function_doc(\"getDefaultSrc\")` / `get_function_doc(\"getSrc\")` / `get_function_doc(\"createMediaSrcset\")` — exact signatures\n- `get_type_definition(\"IkasImage\")` / `get_type_definition(\"IkasProductImage\")` — exact shapes",
626
+ "description": "How to render images AND VIDEOS from IkasImage, IkasProduct variants, and IkasProductImage wrappers. Covers the canonical variant-to-image chain, the mandatory isVideo branch for product media, all-variant-images galleries, non-product images, background images, and responsive srcsets.",
627
+ "content": "## Image Handling\n\nThe canonical recipe for rendering an image. The product-variant case is 4 hops because each step carries metadata the next doesn't (variant selection → that variant's image set → metadata wrapper → CDN-ready image):\n\n```tsx\nimport {\n getSelectedProductVariant,\n getProductVariantMainImage,\n getDefaultSrc,\n getSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nconst variant = getSelectedProductVariant(product); // IkasProductVariant | undefined\nconst productImage = variant && getProductVariantMainImage(variant); // IkasProductImage | undefined\nconst image = productImage?.image; // IkasImage | undefined — what CDN helpers need\n\n// The main media item can itself be a VIDEO (image.isVideo) — see \"Video Media\" below.\nif (image && !image.isVideo) {\n return (\n <>\n <img src={getDefaultSrc(image)} alt={product.name} /> {/* default size */}\n <img src={getSrc(image, 400)} alt=\"\" /> {/* explicit width — number, not an object */}\n <img srcSet={createMediaSrcset(image)} alt=\"\" /> {/* responsive srcset */}\n </>\n );\n}\n```\n\n## All Variant Images (gallery)\n\nMap over `variant.images` and unwrap each `.image`. `IkasProductImage` carries metadata like `order` and `isMain`; the actual `IkasImage` (what the CDN helpers need) is the `.image` property on each.\n\n```tsx\nconst images: IkasImage[] = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n**This list can contain VIDEOS.** Never render it as plain `<img>` — see below.\n\n## Video Media (`isVideo`)\n\nProduct media mixes images and videos. For a video item the CDN has exactly one URL (the\noriginal `.mp4`): `getDefaultSrc`, `getSrc` and `getThumbnailSrc` all return it, the size\nargument is ignored, and there is **no poster/first-frame URL**. So an `<img>` with a video\nsrc is broken media, and `getThumbnailSrc` in a small box silently downloads the whole video.\n\nEvery media call site branches:\n\n```tsx\n{images.map((img, i) =>\n img.isVideo ? (\n <video\n key={i}\n className=\"gallery__media\"\n src={getDefaultSrc(img)} // not getSrc(img, n) — size is ignored for videos\n muted\n playsInline\n loop\n autoPlay // playing contexts only; omit where the media is a still\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <img\n key={i}\n className=\"gallery__media\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)} // srcset applies to <img> only\n sizes=\"(max-width: 1023px) 100vw, 50vw\"\n alt={img.altText || `${product.name} ${i + 1}`}\n />\n )\n)}\n```\n\n- **Playing contexts** (PDP gallery, product card): `autoPlay muted loop playsInline`.\n- **Still contexts** (thumbnail rail, variant swatch, cart/order line, bundle item): no\n `autoPlay`, use `preload=\"metadata\"` — the browser holds the first frame, which is the only\n poster substitute available. Images in those contexts still use `getThumbnailSrc`, so guard\n it: `const imgSrc = img && !img.isVideo ? getThumbnailSrc(img) : null;`\n\nA theme usually has 8+ such call sites, so factor one media sub-component with an `autoPlay`\nflag instead of repeating the ternary.\n\nThree follow-on traps that pass typecheck and build, and are only caught by eye:\n\n1. `<video>` defaults to `object-fit: contain`, `<img>` to `fill` — set `object-fit: cover`\n explicitly or the video sits differently in the same box.\n2. CSS element selectors skip the video branch: `.card__media-wrap img { … }` never matches.\n Write `.card__media-wrap img, .card__media-wrap video { … }`, or style a shared class.\n3. An autoplaying loop is moving content, so it must honour `prefers-reduced-motion`. The\n `autoplay` attribute has no effect after load — pause via the element ref when the media\n query matches.\n\n## Non-Product Images (logo, banner, IMAGE prop)\n\nThese skip the variant hop — they're already `IkasImage`:\n\n```tsx\n{props.logo && <img src={getDefaultSrc(props.logo)} alt=\"\" />}\n```\n\n## Background Images\n\n```tsx\n<div style={{ backgroundImage: image ? `url(${getDefaultSrc(image)})` : \"none\" }} />\n```\n\n## See Also\n\n- `get_framework_guide(\"common-pitfalls\")` §19 — full property-rename catalog (`selectedVariant`, `mainImage`, `href`, `checkoutUrl`)\n- `get_section_template(\"image-handling\")` — full Product Detail section with gallery + zoom + srcset\n- `get_function_doc(\"getDefaultSrc\")` / `get_function_doc(\"getSrc\")` / `get_function_doc(\"createMediaSrcset\")` — exact signatures\n- `get_type_definition(\"IkasImage\")` / `get_type_definition(\"IkasProductImage\")` — exact shapes",
536
628
  "tags": [
537
629
  "image",
538
630
  "rendering",
@@ -543,8 +635,29 @@
543
635
  "IkasProductImage",
544
636
  "variant",
545
637
  "srcset",
546
- "gallery"
638
+ "gallery",
639
+ "video",
640
+ "isVideo",
641
+ "getThumbnailSrc",
642
+ "poster"
643
+ ]
644
+ },
645
+ "theme-globals": {
646
+ "title": "Theme Global Settings & Design Tokens",
647
+ "description": "Read and create the theme's global variables and design tokens (colors, typography, breakpoints, keyframes, color schemes) from code components",
648
+ "content": "The theme developer defines global settings in the editor's \"Styles\" panel. Code components can READ these at runtime and an AI agent can CREATE/LIST them via MCP tools. Prefer reusing the theme's existing tokens (colors, typography) over hardcoding values — it keeps components consistent with the store's design.\n\n### Read at runtime (in your .tsx)\n\nImport from `@ikas/bp-storefront`. These work in SSR, client hydration, and the editor canvas:\n\n```tsx\nimport {\n getThemeSetting, getThemeSettings, // global variables (Theme Settings)\n getThemeColors, getThemeTypography, // design tokens\n getThemeBreakpoints, getThemeKeyframes, getThemeColorSchemes,\n} from \"@ikas/bp-storefront\";\n\nconst brand = getThemeSetting(\"_AbC123XyZ\")?.value; // key = variableName, from list_theme_globals\nconst colors = getThemeColors(); // [{ id, name, resolved, cssVar }]\n// resolved = concrete value (e.g. \"#ff0000\"); cssVar = \"var(--<id>)\" (scheme-aware).\n// e.g. style={{ color: colors[0].cssVar }} or className={getThemeTypography()[0].className}\n```\n\nShapes: colors → `{ id, name, resolved, cssVar }`; typography → `{ id, name, resolved, className, breakpoints? }`; breakpoints → `{ id, name, width }` (widest first); keyframes → `{ id, name, type, ref }`; color schemes → `{ schemes, values }`. Global variables → `{ name, displayName, type, value }` where `name` is the stable runtime key (`variableName`).\n\n### Responsive breakpoints in CSS\n\n`getThemeBreakpoints()` gives you each breakpoint's `width` in JS, but a CSS media query CANNOT use `var()` in its condition (it parses as valid-looking CSS and then fails SILENTLY). So to make a `@media` query follow a theme breakpoint, write the `min-width`/`max-width` yourself and use a `bp(<breakpointId>)` token for the value — the platform rewrites it to the breakpoint's concrete `<width>px` at render time (in BOTH the editor canvas and the published storefront), against the destination store's LIVE breakpoint width:\n\n```css\n/* styles.css — <breakpointId> from list_theme_globals (theme.breakpoints[].id) */\n.grid { grid-template-columns: repeat(3, 1fr); }\n\n/* applies at <= the breakpoint's width */\n@media (max-width: bp(<breakpointId>)) { .grid { grid-template-columns: 1fr; } }\n\n/* applies at > the breakpoint's width */\n@media (min-width: bp(<breakpointId>)) { .grid { grid-template-columns: repeat(4, 1fr); } }\n```\n\n- `bp(<id>)` resolves to `<width>px`. YOU pick the direction by writing `max-width` or `min-width`.\n- Use the breakpoint **id** (stable across renames), NEVER its name. Get ids from `list_theme_globals` (or the runtime `getThemeBreakpoints()` list).\n- Works anywhere in the condition, including `calc()` — e.g. for a non-overlapping upper bound use `@media (min-width: calc(bp(<id>) + 1px))` so it doesn't collide with a `max-width: bp(<id>)` block at the exact width. Inside `calc()` use PX ARITHMETIC ONLY (`+ 1px`, `* 2`, `- bp(<other>)`): the platform folds it to a concrete px value at render time, and a calc mixing other units (rem/em/vw/%) cannot fold — the whole media block is then silently dropped in production builds.\n- Portable: the token resolves to the destination store's own width, and the breakpoint ships automatically as a dependency when you reference it in CSS. Need one that might not exist everywhere? `create_theme_global` (kind `breakpoint`) it first.\n\n### Using a color scheme\n\nColor schemes have two parts: **slots** (the named color keys, e.g. Background/Text/Primary) and **palettes** (sets of colors for those slots). `getThemeColorSchemes()` returns:\n- `schemes`: the slots — `[{ id, name }]`.\n- `values`: the palettes — `[{ id, name, isDefault, className, colorsByScheme }]`, where `colorsByScheme` maps each **slot id** → `{ resolved, cssVar }`.\n\nA scheme color's `cssVar` is `var(--<slotId>)` and is **palette-scoped**: it only resolves inside an element carrying that palette's `className`. So apply the palette to a wrapper, then reference its slots inside:\n\n```tsx\nconst { values } = getThemeColorSchemes();\nconst palette = values.find((v) => v.isDefault) ?? values[0];\n// Reference slots by their id (from list_theme_globals → colorSchemes.schemes), resolving each\n// slot NAME → id ONCE here. NEVER match a slot by .name at runtime: slot names are NOT unique\n// (installing two design assets can create two \"Background\" slots), so\n// schemes.find(x => x.name === \"Background\") may silently pick the WRONG slot. A slot id is stable,\n// so referencing a slot by id is both correct AND portable.\nconst BACKGROUND = \"aB3xY7kLmN\", TEXT = \"pQ4rS2tUvW\", PRIMARY = \"zX8cV1bN0m\"; // real ids from list_theme_globals\nconst slot = (id: string) => palette?.colorsByScheme[id];\n\n// the palette className makes the slot vars resolve in this subtree\nreturn (\n <section\n className={palette?.className}\n style={{ background: slot(BACKGROUND)?.cssVar, color: slot(TEXT)?.cssVar }}\n >\n <button style={{ background: slot(PRIMARY)?.cssVar }}>Buy</button>\n </section>\n);\n```\n\nUse `colorsByScheme[slotId].resolved` if you need the concrete value instead of the live `var()`. Switching palettes at runtime is just swapping the wrapper's `className`.\n\n### Forcing a palette vs. inheriting the section's scheme\n\nSlot vars (`var(--<slotId>)`) resolve against whichever palette `className` sits on an **ancestor**, so you get two modes:\n\n- **Inherit the section's scheme (default, recommended):** do NOT add a palette `className` — just use the slot `cssVar`s. They resolve to the palette the section is set to. A section's color scheme is chosen in the editor (right panel → \"Color Scheme\"), which renders the section wrapper with that palette's class (`_<schemeId>`); leaving your slots unwrapped lets the merchant re-skin the component by switching the section's scheme, with no code change. The var NAMES are identical across palettes (only the value changes with the active ancestor class), so read the `{slotId → cssVar}` map from any palette and stay unwrapped:\n\n```tsx\nconst { values } = getThemeColorSchemes();\nconst palette = values.find((v) => v.isDefault) ?? values[0]; // any palette; var names match across all\n// slot ids from list_theme_globals → colorSchemes.schemes (resolve name → id once; NEVER match a\n// slot by .name at runtime — names are not unique, so a name lookup can pick the wrong slot).\nconst BACKGROUND = \"aB3xY7kLmN\", TEXT = \"pQ4rS2tUvW\", PRIMARY = \"zX8cV1bN0m\";\nconst slot = (id: string) => palette?.colorsByScheme[id]?.cssVar;\n\n// No palette className here → these inherit the SECTION's active scheme.\nreturn (\n <div style={{ background: slot(BACKGROUND), color: slot(TEXT) }}>\n <button style={{ background: slot(PRIMARY) }}>Buy</button>\n </div>\n);\n```\n\n- **Force a specific palette:** wrap the subtree in that palette's `className` (the example above). Use this only for a region that must always use one fixed palette regardless of the section's scheme.\n\n### Create / list via MCP (requires `ikas theme dev` + connected editor)\n\n- `list_theme_globals` — list every global variable and design token in the project (including ones created manually in the editor). **Call this FIRST** so you reuse existing tokens instead of duplicating them.\n- `create_theme_global` — create one, selected by `kind`:\n - `globalVariable` — `display_name` + `type` (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW|ENUM); `value` optional. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: boolean; IMAGE: `{ url }`; BORDER: `{ width: { value, unit }, style, color }`; SHADOW: `{ x, y, blur, spread, color, position: \"outside\"|\"inside\" }`; ENUM: `enum_type_id` (a project enum id from `list_theme_globals` → `enumTypes[]`) + `value` = one of that enum's option VALUES (not a label). An ENUM setting reads back as `{ type: \"ENUM\", value, enumTypeId, options }`, an ENUM prop of the same enum can be bound to it with `bind_global`, and `themeValue(\"_<id>\")` from `./global-types` narrows to the enum's union type.\n - `color` — `name` + `value` (hex).\n - `typography` — `name` + any of `font_family`/`font_size`/`font_weight`/`line_height`/`letter_spacing`.\n - `breakpoint` — `name` + `width`.\n - `keyframe` — `name` + `points` (`[{ point, styles? }]`); each style is `{ property, value }` with a CSS property name (opacity, transform, filter, background, color, …). Apply the keyframe's `ref` as a CSS `animation-name` and set timing (duration/iteration) where you apply it.\n - `colorScheme` — `name` + `colors` (`[{ slotId? | newSlotName?, value }]`); target an existing slot by `slotId` (its id from list_theme_globals → colorSchemes.schemes / colorsByScheme keys), or create/reuse one by `newSlotName` (use the standard slot names like \"Background\"/\"Text\"/\"PrimaryButton/Background\"). Exactly one per entry.\n\n**CLI equivalents** (if you call the ikas-component CLI directly instead of the MCP tools): read with `list-theme-globals`; create with — globalVariable→`create-global-variable`, color→`create-color`, typography→`create-text-style`, breakpoint→`create-breakpoint`, keyframe→`create-keyframe`, colorScheme→`create-color-scheme`. CLI flags are kebab-case (`--display-name`, `--font-size`, `--colors`, …) and the CLI must be run from the project root.\n\n**Update / delete:** `update_theme_global` (fix a global variable's value/type — identify by `name`) and `delete_theme_global` (`kind` globalVariable→`name`, or a design-token kind→`id`). CLI equivalents: `update-global-variable`, `delete-global-variable`, `delete-design-token`.\n\n**Responsive text styles:** `update_text_style` with `breakpoint_id` (an existing breakpoint id from `list_theme_globals`) sets that breakpoint's overrides instead of the desktop values — only `font_size`, `font_weight`, `line_height`, `letter_spacing`; pass `\"\"` to remove an override. CLI: `update-text-style --id <id> --breakpoint <breakpointId> --font-size 32px`. For several breakpoints in ONE call pass `breakpoints: [{ breakpoint_id, font_size?, font_weight?, line_height?, letter_spacing? }]` to `update_text_style` or to `create_theme_global` (kind `typography`, together with the desktop values) — CLI `--breakpoints '[{\"breakpointId\":\"<id>\",\"fontSize\":\"32px\"}]'` on `create-text-style` / `update-text-style`; the whole batch is validated and one invalid entry writes nothing. The editor rejects invalid writes (unknown breakpoint, other properties, italic weight, a property without a desktop value, malformed values, weights the font lacks) — fix the input and retry; breakpoints are not created by this call (use `create_theme_global` kind `breakpoint`).\n\nAfter creating, the new item is readable via the runtime getters above (its key/id comes back in the create result and from `list_theme_globals`).\n\n### Live updates vs snapshots (important)\n\nTheme settings reach your component through two channels — pick the right one or edits won't reflect live:\n\n- **Live (CSS):** use `cssVar` (colors, and color-scheme colors) and `className` (typography text styles). These update **instantly** when the value is edited in the editor, because they map to CSS the editor regenerates live. Prefer these for anything visual.\n - Colors: `style={{ color: token.cssVar }}` (or `background`).\n - Typography: apply the text style's class — `className={t.className}` — do NOT spread `t.resolved` into an inline `style` if you want live updates.\n Text styles can be responsive (per-breakpoint font size, line height, letter spacing and font weight). Apply them with `className` (or the literal `_<id>` class) to get per-breakpoint values automatically. `resolved` contains desktop values only; a responsive style also has `breakpoints: [{ id, width, overrides, resolved }]` (widest first, only breakpoints with own overrides; `overrides` = that breakpoint's own values, `resolved` = full effective style there). Inheritance is chained: a property a breakpoint does not override comes from the nearest WIDER breakpoint that overrides it, else from desktop. Overriding font weight keeps the style's italic (italic is not per breakpoint). `text_overflow: ellipsis` only truncates together with `white_space: nowrap` (a text style property) and `overflow: hidden` (component CSS — not a text style property). The field is absent when the style has no overrides. When a component is published to another store, the breakpoints its text style overrides use are shipped with it.\n - Color-scheme colors are scoped to the palette: a `var(--<key>)` only resolves inside an element carrying that palette's `className`, so wrap the row: `<div className={paletteValue.className}>…<span style={{background: colorsByScheme[keyId].cssVar}}/>…</div>`.\n- **Snapshot (JS):** `resolved` values, breakpoint `width`, keyframe metadata, and global-variable `value`s are read from JS at render time. They reflect editor edits only when the component **re-renders/remounts** (e.g. its props change, or another action refreshes the canvas) — NOT instantly on a theme-token edit. This is expected, not a bug. Global-variable value edits do trigger a canvas refresh, so they reflect; design-token `resolved` values lag until the next refresh.\n\nRule of thumb: for visuals that must track editor edits live, reference `cssVar` / apply `className`; treat `resolved` and `value` as point-in-time reads.\n\n### Portability (shipping as a partner design asset)\n\nWhen this component is published as a partner design asset and installed into another store, the theme globals it uses are detected at publish time and travel with it. Publish scans the component's compiled output AND the shared chunks it imports; each detected global is added to the asset and created in the destination store on install (unless it is already there), so the references in your code keep resolving there.\n\n- **Global variables travel — hardcoded keys included.** A `themeValue(\"_xyz\")` / `getThemeSetting(\"_xyz\")` / `getThemeSettingValue(\"_xyz\")` call with a literal key ships exactly those variables — default value, groups and types included — also when the call lives in a shared helper file. If no literal key is visible (the key is computed or passed in as a value), or the component calls the iterate-all `getThemeSettings()`, ALL global variables ship. A prop bound to a global variable (via its `privateVarMap`) ships too. You do NOT need to turn a setting into a prop just to make it portable.\n- **Don't mix literal and runtime-built keys.** As soon as ANY literal key is visible in the component's compiled code or the shared chunks it imports, only the literal keys ship — a key built at runtime (`\"_\" + id`, a key read from a prop or another value) is then NOT shipped and resolves to nothing in the destination store. Keep every key literal, or read through `getThemeSettings()`.\n- **Colors, typography, keyframes and breakpoints travel too.** A literal reference in your CSS/JS — a color's `cssVar`, a typography `className` (`_<id>`), a keyframe `ref` (`_<id>`), a `bp(<id>)` token — is detected and ships; the iterate-all accessors (`getThemeColors()`, `getThemeTypography()`, `getThemeKeyframes()`, `getThemeBreakpoints()`) ship the whole category. Iterate the runtime lists when the component should adapt to whatever tokens the destination theme has, rather than to the ones it was built against.\n- **Color schemes always travel in full.** Every scheme slot and palette ships with the asset and is matched by id on install, never by name — so referencing a slot by `var(--<slotId>)` / `colorsByScheme[slotId]` stays valid across stores; resolving it by `.name` does not.\n- **Also travels automatically:** types referenced by your props, nested code components, and shared modules.\n\nRule of thumb: reference globals by the ids/keys `list_theme_globals` returns (never by display name) and they ship with the asset — do not tell the user they must recreate them in the destination store.",
649
+ "tags": [
650
+ "theme",
651
+ "global variables",
652
+ "design tokens",
653
+ "colors",
654
+ "typography",
655
+ "breakpoints",
656
+ "keyframes",
657
+ "color schemes",
658
+ "getThemeSetting",
659
+ "getThemeColors"
547
660
  ]
548
661
  }
549
662
  }
550
- }
663
+ }