@ikas/code-components-mcp 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/framework.json +68 -8
- package/data/migration.json +9 -3
- package/data/section-templates/add-to-cart/index.tsx +16 -2
- package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/add-to-cart/utils/cartLimits.ts +23 -0
- package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +57 -24
- package/data/section-templates/cart-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/header-section/sub-components/CartItem/index.tsx +57 -24
- package/data/section-templates/header-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/image-handling/ikas-config-snippet.json +1 -0
- package/data/section-templates/navigation/sub-components/CartItem/index.tsx +57 -24
- package/data/section-templates/navigation/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +16 -2
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +92 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +159 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +141 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +47 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +25 -0
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +1 -0
- package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +120 -0
- package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +70 -0
- package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/product-detail-section/utils/cartLimits.ts +23 -0
- package/data/storefront-api.json +82 -12
- package/data/storefront-types.json +2747 -22
- package/dist/index.js +582 -179
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/framework.json
CHANGED
|
@@ -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\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).",
|
|
21
21
|
"tags": [
|
|
22
22
|
"config",
|
|
23
23
|
"configuration",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"prop-types": {
|
|
36
36
|
"title": "Available Prop Types",
|
|
37
37
|
"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). 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 | `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 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>\u2026</svg> string (SVG) or an array of them (SVG_LIST). It must be a single well-formed <svg> root, \u2264 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. Example: \"defaultValue\": \"<svg viewBox=\\\"0 0 24 24\\\"><path d=\\\"M4 12h16\\\"/></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### 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`, `EXTERNAL`, `FILE`.\n- `EXTERNAL` → set `externalLink` (a URL). `PAGE` → set `pageType` (e.g. `INDEX`; add `pageId` for a specific page). `FILE` → set `fileUrl`.\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### 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.",
|
|
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. 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>\u2026</svg> string (SVG) or an array of them (SVG_LIST). It must be a single well-formed <svg> root, \u2264 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### 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
39
|
"tags": [
|
|
40
40
|
"props",
|
|
41
41
|
"types",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"RICH_TEXT",
|
|
45
45
|
"NUMBER",
|
|
46
46
|
"NUMBER_RANGE",
|
|
47
|
+
"numberRangeData",
|
|
48
|
+
"IkasNumberRange",
|
|
47
49
|
"BOOLEAN",
|
|
48
50
|
"VIDEO",
|
|
49
51
|
"DATE",
|
|
@@ -54,6 +56,13 @@
|
|
|
54
56
|
"COLOR",
|
|
55
57
|
"PRODUCT",
|
|
56
58
|
"PRODUCT_ATTRIBUTE",
|
|
59
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
60
|
+
"IkasProductAttributeDetail",
|
|
61
|
+
"IkasProductAttributeList",
|
|
62
|
+
"getAttributeDetailValues",
|
|
63
|
+
"getAttributeListValues",
|
|
64
|
+
"attributePropValue",
|
|
65
|
+
"attributeListPropValue",
|
|
57
66
|
"CATEGORY",
|
|
58
67
|
"BRAND",
|
|
59
68
|
"BLOG",
|
|
@@ -142,7 +151,7 @@
|
|
|
142
151
|
"imports": {
|
|
143
152
|
"title": "Import Patterns",
|
|
144
153
|
"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`).\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`",
|
|
154
|
+
"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.)",
|
|
146
155
|
"tags": [
|
|
147
156
|
"import",
|
|
148
157
|
"storefront",
|
|
@@ -223,7 +232,7 @@
|
|
|
223
232
|
"form-handling": {
|
|
224
233
|
"title": "Form Handling",
|
|
225
234
|
"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 | `getCouponCodeForm(customerStore)` → `initCouponCodeForm(form)` | `setCouponCodeFormCouponCode(form, value)` | `submitCouponCodeForm(form)` (remove with `removeCouponCodeForm(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",
|
|
235
|
+
"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 | `getCouponCodeForm(customerStore)` → `initCouponCodeForm(form)` | `setCouponCodeFormCouponCode(form, value)` | `submitCouponCodeForm(form)` (remove with `removeCouponCodeForm(form)`) |\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` \u002d\u002d you cannot lay it out statically. Call\n`get_framework_guide({ topic: \"address\u002dform\u002dpatterns\" })` 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
236
|
"tags": [
|
|
228
237
|
"form",
|
|
229
238
|
"login",
|
|
@@ -249,7 +258,7 @@
|
|
|
249
258
|
"product-detail-patterns": {
|
|
250
259
|
"title": "Product Detail Patterns (Serel Reference)",
|
|
251
260
|
"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.",
|
|
261
|
+
"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
262
|
"tags": [
|
|
254
263
|
"product",
|
|
255
264
|
"detail",
|
|
@@ -264,6 +273,36 @@
|
|
|
264
273
|
"serel"
|
|
265
274
|
]
|
|
266
275
|
},
|
|
276
|
+
"back-in-stock-patterns": {
|
|
277
|
+
"title": "Back-in-Stock Notification Patterns",
|
|
278
|
+
"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",
|
|
279
|
+
"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 |",
|
|
280
|
+
"tags": [
|
|
281
|
+
"back-in-stock",
|
|
282
|
+
"notify-me",
|
|
283
|
+
"out-of-stock",
|
|
284
|
+
"variant",
|
|
285
|
+
"form",
|
|
286
|
+
"reminder",
|
|
287
|
+
"product-detail",
|
|
288
|
+
"conversion"
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
"product-options": {
|
|
292
|
+
"title": "Product Options (Customization) Patterns",
|
|
293
|
+
"description": "Personalization option sets: async load, type-aware rendering for all option types, conditional child options, file upload, and submit-time validation with reset",
|
|
294
|
+
"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.",
|
|
295
|
+
"tags": [
|
|
296
|
+
"product-options",
|
|
297
|
+
"customization",
|
|
298
|
+
"personalization",
|
|
299
|
+
"option-set",
|
|
300
|
+
"file-upload",
|
|
301
|
+
"child-options",
|
|
302
|
+
"validation",
|
|
303
|
+
"product-detail"
|
|
304
|
+
]
|
|
305
|
+
},
|
|
267
306
|
"product-list-patterns": {
|
|
268
307
|
"title": "Product List & Filtering Patterns",
|
|
269
308
|
"description": "Category pages, product filtering with display-type-aware rendering (swatch, number range, checkbox), sorting, pagination, and search patterns",
|
|
@@ -283,7 +322,7 @@
|
|
|
283
322
|
"cart-patterns": {
|
|
284
323
|
"title": "Cart Management Patterns (Serel Reference)",
|
|
285
324
|
"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\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
|
|
325
|
+
"content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Quantity Limits (min/max per cart)\n\nA product's sales channel can cap how many units go into one cart. The limits live on the product's `salesChannels`, matched to the active channel:\n\n```tsx\nimport { IkasStorefrontConfig } from \"@ikas/bp-storefront\";\n\nconst channel = product.salesChannels?.find((sc) => sc.id === IkasStorefrontConfig.salesChannelId);\nconst min = channel?.minQuantityPerCart ?? 1;\nconst max = channel?.maxQuantityPerCart; // null/undefined = no cap\n```\n\nApply these to the quantity stepper: seed the initial quantity to `min`, and cap increments at `max`. `addItemToCart(variant, product, qty)` also clamps to the same limits internally — but silently, so enforce them in the UI too or the customer sees the number snap with no explanation.\n\n> `IkasStorefrontConfig.salesChannelId` is a property. The older `getSalesChannelId()` method does not exist in this SDK.\n\n### Cart Operation validationError (complete list)\n\n`addItemToCart` / `changeItemQuantity` return `{ success, validationError? }`. The full set of `validationError` values is exactly three:\n\n| Value | Meaning |\n|-------|---------|\n| `INSUFFICIENT_STOCK` | Not enough stock for the requested quantity |\n| `INVALID_PRODUCT_OPTION_VALUES` | Required product options not fully or validly selected |\n| `EMPTY_CART` | The operation needs a non-empty cart |\n\nThere is **no** `MAX_QUANTITY_PER_CART_*` / `MIN_QUANTITY_PER_CART_*` error — quantity limits are clamped silently (see above), never surfaced as a validation error. Prevent limit violations in the stepper rather than branching on error codes that do not exist.\n\n### Campaign Offer Lines in the Cart\n\nCampaign discounts surface as order-level adjustment rows, not as edits on the line items:\n\n```tsx\n{cart.orderAdjustments?.map((adj) => (\n <div key={adj.name} className=\"cart-adjustment\">\n <span>{adj.name}</span>\n <span>{formatCurrency(adj.amount, adj.currencyCode, adj.currencySymbol)}</span>\n </div>\n))}\n```\n\nWhen a campaign auto-adds a bundled or gift line (a \"buy 1 get N\" offer), `isOrderLineItemAutoCreated(cart, item)` returns `true` for that line. Use it to badge the line and hide its quantity/remove controls — the customer cannot edit an auto-created line directly:\n\n```tsx\nimport { isOrderLineItemAutoCreated } from \"@ikas/bp-storefront\";\n\n{lineItems.map((item) => (\n <CartItem key={item.id} item={item} readOnly={isOrderLineItemAutoCreated(cart, item)} />\n))}\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
|
|
287
326
|
"tags": [
|
|
288
327
|
"cart",
|
|
289
328
|
"checkout",
|
|
@@ -296,10 +335,31 @@
|
|
|
296
335
|
"serel"
|
|
297
336
|
]
|
|
298
337
|
},
|
|
338
|
+
"address-form-patterns": {
|
|
339
|
+
"title": "Address Form Patterns",
|
|
340
|
+
"description": "Country-driven address form layout: the addressFormat matrix, the renderFormField dispatch table, free-text fallback, and the country/state/city/district cascade",
|
|
341
|
+
"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.",
|
|
342
|
+
"tags": [
|
|
343
|
+
"address",
|
|
344
|
+
"addressFormat",
|
|
345
|
+
"address-form",
|
|
346
|
+
"form-layout",
|
|
347
|
+
"country",
|
|
348
|
+
"state",
|
|
349
|
+
"city",
|
|
350
|
+
"district",
|
|
351
|
+
"region",
|
|
352
|
+
"postal-code",
|
|
353
|
+
"free-text",
|
|
354
|
+
"checkout",
|
|
355
|
+
"account",
|
|
356
|
+
"renderFormField"
|
|
357
|
+
]
|
|
358
|
+
},
|
|
299
359
|
"account-patterns": {
|
|
300
360
|
"title": "Account & Dashboard Patterns",
|
|
301
361
|
"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
|
|
362
|
+
"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\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 {\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```",
|
|
303
363
|
"tags": [
|
|
304
364
|
"account",
|
|
305
365
|
"orders",
|
|
@@ -549,7 +609,7 @@
|
|
|
549
609
|
"theme-globals": {
|
|
550
610
|
"title": "Theme Global Settings & Design Tokens",
|
|
551
611
|
"description": "Read and create the theme's global variables and design tokens (colors, typography, breakpoints, keyframes, color schemes) from code components",
|
|
552
|
-
"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 → `{ id, name, width }`; 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.\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// and travels unchanged across stores, 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-component 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); `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\" }`.\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\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 - 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, only the globals it DECLARES as dependencies travel with it. Write components so their global usage stays portable:\n\n- **Expose theme global variables as props — don't hardcode keys.** A prop bound to a global variable (via its `privateVarMap`) is detected and ships with the asset. A `themeValue(\"_xyz\")` / `getThemeSetting(\"_xyz\")` call with a hardcoded key in your source is NOT detected, so that variable won't travel and resolves to nothing in the destination store. If you need a theme setting, add it as a prop and read the prop, not a hardcoded key.\n- **Iterate the runtime lists instead of hardcoding token ids.** `getThemeColors()`, `getThemeTypography()`, `getThemeKeyframes()`, `getThemeBreakpoints()`, and `getThemeColorSchemes()` return the DESTINATION store's own tokens — iterate them so the component adapts to whatever theme it lands in. A specific color/keyframe id hardcoded from the list won't exist in another store. (Color-SCHEME SLOT ids are the exception: a scheme travels in full with its slot ids intact and is matched by id on install — never re-keyed — so referencing a slot by `var(--<slotId>)` / `colorsByScheme[slotId]` stays valid across stores; resolving it by `.name` does not.)\n- **Travels automatically:** color schemes (in full — the slot/palette pattern above keeps working after install), types referenced by your props, nested code components, and shared modules.\n\nRule of thumb: read named settings through props and read colors/typography/etc. by iterating the runtime lists — both are portable; a hardcoded global id in source is not.",
|
|
612
|
+
"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 → `{ id, name, width }`; 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// and travels unchanged across stores, 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-component 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); `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\" }`.\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\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 - 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, only the globals it DECLARES as dependencies travel with it. Write components so their global usage stays portable:\n\n- **Expose theme global variables as props — don't hardcode keys.** A prop bound to a global variable (via its `privateVarMap`) is detected and ships with the asset. A `themeValue(\"_xyz\")` / `getThemeSetting(\"_xyz\")` call with a hardcoded key in your source is NOT detected, so that variable won't travel and resolves to nothing in the destination store. If you need a theme setting, add it as a prop and read the prop, not a hardcoded key.\n- **Iterate the runtime lists instead of hardcoding token ids.** `getThemeColors()`, `getThemeTypography()`, `getThemeKeyframes()`, `getThemeBreakpoints()`, and `getThemeColorSchemes()` return the DESTINATION store's own tokens — iterate them so the component adapts to whatever theme it lands in. A specific color/keyframe id hardcoded from the list won't exist in another store. (Color-SCHEME SLOT ids are the exception: a scheme travels in full with its slot ids intact and is matched by id on install — never re-keyed — so referencing a slot by `var(--<slotId>)` / `colorsByScheme[slotId]` stays valid across stores; resolving it by `.name` does not.)\n- **Travels automatically:** color schemes (in full — the slot/palette pattern above keeps working after install), types referenced by your props, nested code components, and shared modules.\n\nRule of thumb: read named settings through props and read colors/typography/etc. by iterating the runtime lists — both are portable; a hardcoded global id in source is not.",
|
|
553
613
|
"tags": [
|
|
554
614
|
"theme",
|
|
555
615
|
"global variables",
|