@ikas/code-components-mcp 1.4.0-beta.150 → 1.4.0-beta.152
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 +9 -5
- package/data/storefront-api.json +5 -5
- package/data/storefront-types.json +33 -17
- package/dist/index.js +41 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/framework.json
CHANGED
|
@@ -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. 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```",
|
|
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>…</svg> string (SVG) or an array of them (SVG_LIST). It must be a single well-formed <svg> root, ≤ 64 KB, with no embedded base64 raster images; it is sanitized on load. Invalid defaults are rejected by the CLI (add-prop / add-component hard-fail with the reason) and silently dropped by the editor when the component is placed, so verify your default is valid. It must also RENDER VISIBLY: an open path with no enclosed area (e.g. the single horizontal line M4 12h16) is invisible under the default fill:black; stroke:none. Give the shape real area and set a color - prefer fill=currentColor (or an explicit stroke with stroke-width) so the icon inherits the theme text color. Example: \"defaultValue\": \"<svg viewBox=\\\"0 0 24 24\\\"><path fill=\\\"currentColor\\\" d=\\\"M12 2 2 22h20z\\\"/></svg>\".\n\n### SVG normalization & sanitization -- the theme-developer standard\n\n**Responsibility split (read this first):**\n- **Security/sanitization is the editor's job, not yours.** Every SVG is sanitized (DOMPurify) on upload, on programmatic writes, AND at render -- you never sanitize in component code.\n- **Normalization (size / color / id-scoping) is `normalizeSvg`'s job.** The theme's built-in SVG element applies it automatically; in your own code component you MUST call `normalizeSvg(value, { idPrefix })` before `dangerouslySetInnerHTML`. There is ONE shared util -- do not reinvent per theme: `import { normalizeSvg } from \"@ikas/bp-storefront\"`.\n\n`normalizeSvg(svg, options)` options: `{ idPrefix: string; scopeIds?: boolean = true; color?: \"currentColor\"; size?: boolean = true }`.\n\n**1. Size.** `size: true` (default) synthesizes a `viewBox` from numeric (unitless/px) `width`/`height` when missing, then strips `width`/`height` so CSS controls size. If there is no viewBox AND size cannot be synthesized (e.g. `%`/`em` dims), width/height are kept (so the icon is not left with zero intrinsic size). Merchants are NOT required to upload viewBox'd or size-less SVGs -- normalize handles it. Size icons with CSS: `.icon svg { width: 24px; height: 24px; }`.\n\n**2. Color.** Opt-in `{ color: \"currentColor\" }` rewrites `fill`/`stroke` (attributes AND `style:` declarations) to `currentColor`, but PRESERVES `none` and `url(...)` (gradients/patterns). So outline icons (`fill=\"none\" stroke=\"...\"`) keep `fill:none` and get `stroke:currentColor` -- no special-casing needed. For MULTI-COLOR SVGs, OMIT `color` to keep the original colors. Rule of thumb: single-color icons -> pass `currentColor`; multi-color -> omit.\n\n**3. Collision (id / clipPath / url(#...)).** Solved in code, not by the editor. `scopeIds: true` (default) prefixes every declared `id` and every `url(#...)` / `href=\"#...\"` reference with `idPrefix`, so the same SVG rendered multiple times (or multiple SVG_LIST items) cannot collide. Give each render an instance-unique prefix -- the built-in element uses its element id; for SVG_LIST use a per-item prefix like `` `${propKey}-${i}` ``.\n\n**4. Sanitization -- what the editor strips.** Beyond the 64 KB cap and base64-raster rejection, the editor strips `<script>`, `on*` event handlers (onload/onclick/onerror/...), `javascript:` hrefs, and `<foreignObject>`. This runs on file upload, on programmatic `update_section_prop` writes, and again at render -- so stored and rendered markup is always safe. `normalizeSvg` is NOT a sanitizer (pure string transforms); security lives in the separate sanitize/validate layer and is automatic.\n\n**5. One shared pattern.** Yes -- `normalizeSvg` from `@ikas/bp-storefront` is the single, isomorphic (SSR/browser/node) util. Do not hand-roll size/color/id-scoping per theme.\n\n**Naming.** The editor shows each SVG's `class` attribute as its display name in the list; SVG / SVG_LIST items with no class render as `svg-1`, `svg-2`, .... Set `class=\"my-icon\"` in the markup to name it.\n\n### NUMBER_RANGE prop (bounded numeric slider)\n\nA `NUMBER_RANGE` prop renders a **slider + number input** and gives the store owner one bounded number with an optional unit. Configure the slider with a **required** `numberRangeData` object on the prop (`min` and `max` are mandatory; `interval` and `unit` are optional). A NUMBER_RANGE prop authored without it is rejected:\n\n- `min` (integer) — lower bound\n- `max` (integer) — upper bound, must be greater than `min`\n- `interval` (positive integer, optional) — the step increment; typed values snap to `min + k·interval`, and a `0`/negative interval is dropped (step falls back to 1)\n- `unit` (string, optional) — suffix shown next to the number input (e.g. `\"px\"`, `\"%\"`)\n\n**Runtime read shape** is `IkasNumberRange`: read `props.myRange.value` (a number) and `props.myRange.unit` (string or null) — e.g. `props.gap.value + (props.gap.unit ?? \"px\")`.\n\n**Authored `defaultValue`** is a typed object `{ \"value\": <number>, \"unit\": \"px\" | null }` — NOT a bare number and NOT a JSON string. When the prop defines `numberRangeData`, the CLI validates that `value` is within `[min, max]` and lands on the interval grid (`min + k·interval`); off-range or off-step defaults are rejected.\n\n**Config example:**\n```json\n{ \"name\": \"gap\", \"displayName\": \"Gap\", \"type\": \"NUMBER_RANGE\", \"numberRangeData\": { \"min\": 0, \"max\": 100, \"interval\": 5, \"unit\": \"px\" }, \"defaultValue\": { \"value\": 20, \"unit\": \"px\" } }\n```\n\n**CLI command:**\n```bash\nnpx ikas-component config add-prop --component MyComp --name gap --displayName Gap --type NUMBER_RANGE --numberRangeData '{\"min\":0,\"max\":100,\"interval\":5,\"unit\":\"px\"}' --defaultValue '{\"value\":20,\"unit\":\"px\"}'\n```\n\n**Update the config or default later** (send a new `--numberRangeData` to change it; `--numberRangeData none` is rejected while the prop stays NUMBER_RANGE, since min/max are required):\n```bash\nnpx ikas-component config update-prop --component MyComp --prop gap --numberRangeData '{\"min\":0,\"max\":200,\"interval\":10,\"unit\":\"px\"}'\n```\n\n### PRODUCT_ATTRIBUTE and PRODUCT_ATTRIBUTE_LIST props (product custom fields)\n\n**CRITICAL — these props do NOT hand you attribute values.** What your component receives is a *wrapper* pairing the picker selection with the product it resolves against. Reading `prop.value` (or mapping over the prop) references fields that do not exist at runtime: `check` and `build` stay clean, every field is `undefined`, and the component silently renders nothing.\n\nThree distinct shapes are involved — do not confuse them:\n\n| Where | Shape |\n|-------|-------|\n| Value stored by the editor (visible via `get_section_values`) | `{ \"attributeId\": \"…\", \"usePageData\": true }` — the list variant uses `attributeIds: [...]` |\n| Runtime prop your component receives | `IkasProductAttributeDetail` = `{ product, attributePropValue }` — the list variant is `IkasProductAttributeList` = `{ product, attributeListPropValue }` |\n| Resolved attribute values | Reachable ONLY through the helpers below: `getAttributeDetailValues()` → `IkasProductAttributeValue[]`, `getAttributeListValues()` → `IkasProductAttributeMap[]` (each entry is `{ attribute, values }`) |\n\n**Read them with the helpers from `@ikas/bp-storefront`:**\n```tsx\nimport { getAttributeDetailValues, getAttributeListValues } from \"@ikas/bp-storefront\";\n\n// PRODUCT_ATTRIBUTE → attribute: IkasProductAttributeDetail | null\nif (!attribute?.attributePropValue?.attributeId) return null;\nconst html = getAttributeDetailValues(attribute)\n .map((v) => v.value)\n .filter(Boolean)\n .join(\"\");\n\n// PRODUCT_ATTRIBUTE_LIST → attributeList: IkasProductAttributeList\n// getAttributeListValues returns IkasProductAttributeMap[] — grouped per attribute, NOT a flat value list\nconst groups = getAttributeListValues(attributeList);\ngroups.forEach((g) => console.log(g.attribute.name, g.values));\n```\n\n**Second trap — a resolved value's `value` is often `null`.** `getAttributeDetailValues()` returning items does NOT mean you have text to render. For the choice-style attribute types (`CHOICE`, `MULTIPLE_CHOICE`, `COLOR_THUMBNAIL_MULTIPLE_CHOICE`) the selection lives in `productAttributeOptionId` / `productAttributeOption` and `value` is `null` by design, so `.map((v) => v.value)` yields empty strings and the component looks broken again. Read the label defensively:\n\n```tsx\nconst text = getAttributeDetailValues(attribute)\n .map((v) => v.value ?? v.productAttributeOption?.name ?? \"\")\n .filter(Boolean)\n .join(\", \");\n```\n\nWhich field carries the content depends on `v.productAttribute?.type`: `TEXT`/`NUMERIC`/`HTML`/`DATE`/`DATETIME`/`BOOLEAN` → `value`; the choice types → `productAttributeOption.name`; `IMAGE` → `images` / `imageIds`; `TABLE` → parse with `getIkasProductAttributeTableValue(v)`; `PRODUCT` → `getIkasProductAttrributeProductIds(v)` (that export name really is misspelled — see `get_framework_guide(\"imports\")`). Check the type with `get_type_definition(\"IkasProductAttributeValue\")` before assuming a field is populated.\n\n`PRODUCT_ATTRIBUTE_LIST` is a **single object, not an array** — `attributeList.map(...)` throws at runtime; the selected ids live at `attributeList.attributeListPropValue.attributeIds`. Both prop types are merchant-data types, so neither can carry a `defaultValue`. Details: `get_function_doc(\"getAttributeDetailValues\")`, `get_type_definition(\"IkasProductAttributeDetail\")`, `get_type_definition(\"IkasProductAttributeList\")`.\n\n### LINK and LIST_OF_LINK props (navigation links)\n\n**CRITICAL — the authored `defaultValue` shape is NOT the runtime read shape.** In component code you READ a link as `props.myLink.href` / `.label` / `.subLinks`. But the `defaultValue` you author in `ikas.config.json` MUST be the typed object shown below. Do NOT pass `{ \"href\": \"…\", \"label\": \"…\" }` (legacy shape) and do NOT pass the value as a JSON string — the CLI rejects both.\n\n**LINK** `defaultValue` is a single link object:\n```json\n{ \"linkType\": \"EXTERNAL\", \"label\": \"Shop now\", \"externalLink\": \"https://example.com\", \"subLinks\": [] }\n```\nor, to link to a store page:\n```json\n{ \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] }\n```\n\n**LIST_OF_LINK** `defaultValue` wraps an array of link objects in `{ \"links\": [...] }`:\n```json\n{ \"links\": [\n { \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] },\n { \"linkType\": \"EXTERNAL\", \"label\": \"Blog\", \"externalLink\": \"https://example.com/blog\", \"subLinks\": [] }\n] }\n```\n\n**Rules for every link object:**\n- `linkType` is REQUIRED and must be one of `PAGE` or `EXTERNAL`. `FILE` links are NOT authorable in a code-component default (they reference a store-specific uploaded file) — the CLI rejects them.\n- Each link MUST carry a destination for its `linkType` — the CLI rejects a link that points nowhere. `EXTERNAL` → a non-empty `externalLink` URL. `PAGE` → a `pageType` (e.g. `INDEX`); do NOT invent a `pageId` — it is per-store and filled automatically when the theme is installed, so author only `pageType`.\n- `subLinks` must be an array (use `[]` when empty); each entry is itself a link object with the same shape (for dropdown/nested menus).\n- NEVER use a JSON string, and NEVER use the legacy `{ label, href }` shape.\n\n**Config example (LINK):**\n```json\n{ \"name\": \"ctaLink\", \"displayName\": \"CTA Link\", \"type\": \"LINK\", \"defaultValue\": { \"linkType\": \"EXTERNAL\", \"label\": \"Shop now\", \"externalLink\": \"https://example.com\", \"subLinks\": [] } }\n```\n\n**Config example (LIST_OF_LINK):**\n```json\n{ \"name\": \"menuLinks\", \"displayName\": \"Menu Links\", \"type\": \"LIST_OF_LINK\", \"defaultValue\": { \"links\": [ { \"linkType\": \"PAGE\", \"label\": \"Home\", \"pageType\": \"INDEX\", \"subLinks\": [] } ] } }\n```\n\n**CLI command (LIST_OF_LINK):**\n```bash\nnpx ikas-component config add-prop --component MyComp --name menuLinks --displayName \"Menu Links\" --type LIST_OF_LINK --defaultValue '{\"links\":[{\"linkType\":\"PAGE\",\"label\":\"Home\",\"pageType\":\"INDEX\",\"subLinks\":[]}]}'\n```\n\n### Prop grouping\nProps can be assigned to groups via `groupId` for organized editor sidebar display. See `get_framework_guide(\"prop-groups\")` for details.\n\n### 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",
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
"form-handling": {
|
|
233
233
|
"title": "Form Handling",
|
|
234
234
|
"description": "How to use the form model pattern for login, registration, address, and other forms",
|
|
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`
|
|
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` -- you cannot lay it out statically. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building any address form.\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
|
|
236
236
|
"tags": [
|
|
237
237
|
"form",
|
|
238
238
|
"login",
|
|
@@ -591,8 +591,8 @@
|
|
|
591
591
|
},
|
|
592
592
|
"image-handling": {
|
|
593
593
|
"title": "Image Handling",
|
|
594
|
-
"description": "How to render images from IkasImage, IkasProduct variants, and IkasProductImage wrappers. Covers the canonical variant-to-image chain, all-variant-images
|
|
595
|
-
"content": "## Image Handling\n\nThe canonical recipe for rendering an image. The product-variant case is 4 hops because each step carries metadata the next doesn't (variant selection → that variant's image set → metadata wrapper → CDN-ready image):\n\n```tsx\nimport {\n getSelectedProductVariant,\n getProductVariantMainImage,\n getDefaultSrc,\n getSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nconst variant = getSelectedProductVariant(product); // IkasProductVariant | undefined\nconst productImage = variant && getProductVariantMainImage(variant); // IkasProductImage | undefined\nconst image = productImage?.image; // IkasImage | undefined — what CDN helpers need\n\nif (image) {\n return (\n <>\n <img src={getDefaultSrc(image)} alt={product.name} /> {/* default size */}\n <img src={getSrc(image,
|
|
594
|
+
"description": "How to render images AND VIDEOS from IkasImage, IkasProduct variants, and IkasProductImage wrappers. Covers the canonical variant-to-image chain, the mandatory isVideo branch for product media, all-variant-images galleries, non-product images, background images, and responsive srcsets.",
|
|
595
|
+
"content": "## Image Handling\n\nThe canonical recipe for rendering an image. The product-variant case is 4 hops because each step carries metadata the next doesn't (variant selection → that variant's image set → metadata wrapper → CDN-ready image):\n\n```tsx\nimport {\n getSelectedProductVariant,\n getProductVariantMainImage,\n getDefaultSrc,\n getSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nconst variant = getSelectedProductVariant(product); // IkasProductVariant | undefined\nconst productImage = variant && getProductVariantMainImage(variant); // IkasProductImage | undefined\nconst image = productImage?.image; // IkasImage | undefined — what CDN helpers need\n\n// The main media item can itself be a VIDEO (image.isVideo) — see \"Video Media\" below.\nif (image && !image.isVideo) {\n return (\n <>\n <img src={getDefaultSrc(image)} alt={product.name} /> {/* default size */}\n <img src={getSrc(image, 400)} alt=\"\" /> {/* explicit width — number, not an object */}\n <img srcSet={createMediaSrcset(image)} alt=\"\" /> {/* responsive srcset */}\n </>\n );\n}\n```\n\n## All Variant Images (gallery)\n\nMap over `variant.images` and unwrap each `.image`. `IkasProductImage` carries metadata like `order` and `isMain`; the actual `IkasImage` (what the CDN helpers need) is the `.image` property on each.\n\n```tsx\nconst images: IkasImage[] = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n**This list can contain VIDEOS.** Never render it as plain `<img>` — see below.\n\n## Video Media (`isVideo`)\n\nProduct media mixes images and videos. For a video item the CDN has exactly one URL (the\noriginal `.mp4`): `getDefaultSrc`, `getSrc` and `getThumbnailSrc` all return it, the size\nargument is ignored, and there is **no poster/first-frame URL**. So an `<img>` with a video\nsrc is broken media, and `getThumbnailSrc` in a small box silently downloads the whole video.\n\nEvery media call site branches:\n\n```tsx\n{images.map((img, i) =>\n img.isVideo ? (\n <video\n key={i}\n className=\"gallery__media\"\n src={getDefaultSrc(img)} // not getSrc(img, n) — size is ignored for videos\n muted\n playsInline\n loop\n autoPlay // playing contexts only; omit where the media is a still\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <img\n key={i}\n className=\"gallery__media\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)} // srcset applies to <img> only\n sizes=\"(max-width: 1023px) 100vw, 50vw\"\n alt={img.altText || `${product.name} ${i + 1}`}\n />\n )\n)}\n```\n\n- **Playing contexts** (PDP gallery, product card): `autoPlay muted loop playsInline`.\n- **Still contexts** (thumbnail rail, variant swatch, cart/order line, bundle item): no\n `autoPlay`, use `preload=\"metadata\"` — the browser holds the first frame, which is the only\n poster substitute available. Images in those contexts still use `getThumbnailSrc`, so guard\n it: `const imgSrc = img && !img.isVideo ? getThumbnailSrc(img) : null;`\n\nA theme usually has 8+ such call sites, so factor one media sub-component with an `autoPlay`\nflag instead of repeating the ternary.\n\nThree follow-on traps that pass typecheck and build, and are only caught by eye:\n\n1. `<video>` defaults to `object-fit: contain`, `<img>` to `fill` — set `object-fit: cover`\n explicitly or the video sits differently in the same box.\n2. CSS element selectors skip the video branch: `.card__media-wrap img { … }` never matches.\n Write `.card__media-wrap img, .card__media-wrap video { … }`, or style a shared class.\n3. An autoplaying loop is moving content, so it must honour `prefers-reduced-motion`. The\n `autoplay` attribute has no effect after load — pause via the element ref when the media\n query matches.\n\n## Non-Product Images (logo, banner, IMAGE prop)\n\nThese skip the variant hop — they're already `IkasImage`:\n\n```tsx\n{props.logo && <img src={getDefaultSrc(props.logo)} alt=\"\" />}\n```\n\n## Background Images\n\n```tsx\n<div style={{ backgroundImage: image ? `url(${getDefaultSrc(image)})` : \"none\" }} />\n```\n\n## See Also\n\n- `get_framework_guide(\"common-pitfalls\")` §19 — full property-rename catalog (`selectedVariant`, `mainImage`, `href`, `checkoutUrl`)\n- `get_section_template(\"image-handling\")` — full Product Detail section with gallery + zoom + srcset\n- `get_function_doc(\"getDefaultSrc\")` / `get_function_doc(\"getSrc\")` / `get_function_doc(\"createMediaSrcset\")` — exact signatures\n- `get_type_definition(\"IkasImage\")` / `get_type_definition(\"IkasProductImage\")` — exact shapes",
|
|
596
596
|
"tags": [
|
|
597
597
|
"image",
|
|
598
598
|
"rendering",
|
|
@@ -603,7 +603,11 @@
|
|
|
603
603
|
"IkasProductImage",
|
|
604
604
|
"variant",
|
|
605
605
|
"srcset",
|
|
606
|
-
"gallery"
|
|
606
|
+
"gallery",
|
|
607
|
+
"video",
|
|
608
|
+
"isVideo",
|
|
609
|
+
"getThumbnailSrc",
|
|
610
|
+
"poster"
|
|
607
611
|
]
|
|
608
612
|
},
|
|
609
613
|
"theme-globals": {
|
package/data/storefront-api.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
2
|
+
"generatedAt": "2026-08-04T09:40:26.614Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListProductBrand",
|
|
@@ -2788,16 +2788,16 @@
|
|
|
2788
2788
|
{
|
|
2789
2789
|
"name": "getThumbnailSrc",
|
|
2790
2790
|
"signature": "function getThumbnailSrc(image: IkasImage): string",
|
|
2791
|
-
"description": "Returns the thumbnail source URL for an
|
|
2791
|
+
"description": "Returns the thumbnail source URL for an IMAGE at 180px resolution.\n\nVIDEOS HAVE NO 180px THUMBNAIL and no poster URL. When `image.isVideo === true` the size is\nIGNORED and this returns the same full-size .mp4 as `getDefaultSrc(image)` — so using it in a\nthumbnail-sized box (swatch, cart line, order line) downloads the whole video instead of a\n180px webp. Nothing looks broken, which makes it a silent performance regression.\n\nBranch on `item.isVideo` first and only pass images here. For the video branch in a small,\nstatic context render `<video src={getDefaultSrc(item)} preload=\"metadata\" muted playsInline>`\nwith no `autoPlay`: the browser fetches only the first frame and holds it as a stand-in poster.",
|
|
2792
2792
|
"params": [
|
|
2793
2793
|
{
|
|
2794
2794
|
"name": "image",
|
|
2795
|
-
"description": "- The image object to generate the thumbnail source URL for."
|
|
2795
|
+
"description": "- The image object to generate the thumbnail source URL for. Videos ignore the size."
|
|
2796
2796
|
}
|
|
2797
2797
|
],
|
|
2798
|
-
"returns": "The CDN URL string
|
|
2798
|
+
"returns": "The CDN URL string: a 180px image, or — for `isVideo` items — the original .mp4.",
|
|
2799
2799
|
"returnType": "string",
|
|
2800
|
-
"example": "```typescript\nimport { getThumbnailSrc, getDefaultSrc } from \"@ikas/bp-storefront\";\n\n// Thumbnail rail — branch on isVideo per item:\n{product.images.map((item, i) =>\n item.isVideo ? (\n <video key={i} src={getDefaultSrc(item)} muted preload=\"metadata\">\n <track kind=\"captions\" />\n </video>\n ) : (\n <img key={i} src={getThumbnailSrc(item)} sizes=\"112px\" alt={`Thumb ${i + 1}`} />\n )\n)}\n```",
|
|
2800
|
+
"example": "```typescript\nimport { getThumbnailSrc, getDefaultSrc } from \"@ikas/bp-storefront\";\n\n// Thumbnail rail — branch on isVideo per item (no autoPlay: first frame acts as the poster):\n{product.images.map((item, i) =>\n item.isVideo ? (\n <video key={i} src={getDefaultSrc(item)} muted playsInline preload=\"metadata\">\n <track kind=\"captions\" />\n </video>\n ) : (\n <img key={i} src={getThumbnailSrc(item)} sizes=\"112px\" alt={`Thumb ${i + 1}`} />\n )\n)}\n```",
|
|
2801
2801
|
"categories": [
|
|
2802
2802
|
"Image"
|
|
2803
2803
|
],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
2
|
+
"generatedAt": "2026-08-04T09:40:26.642Z",
|
|
3
3
|
"types": [
|
|
4
4
|
{
|
|
5
5
|
"name": "TextElementTagTypes",
|
|
@@ -4877,7 +4877,8 @@
|
|
|
4877
4877
|
{
|
|
4878
4878
|
"name": "isVideo",
|
|
4879
4879
|
"type": "boolean | null",
|
|
4880
|
-
"optional": true
|
|
4880
|
+
"optional": true,
|
|
4881
|
+
"description": "True when this media item is a VIDEO, not an image. Product media sets routinely mix both.\ngetDefaultSrc/getSrc/getThumbnailSrc then return an .mp4 URL, so rendering it in an <img>\nproduces broken media — branch on this flag and render <video src={getDefaultSrc(item)}>.\nVideos have no resized variant and no poster URL: the size argument is ignored, and\ncreateMediaSrcset does not apply."
|
|
4881
4882
|
},
|
|
4882
4883
|
{
|
|
4883
4884
|
"name": "altText",
|
|
@@ -9646,7 +9647,8 @@
|
|
|
9646
9647
|
{
|
|
9647
9648
|
"name": "isVideo",
|
|
9648
9649
|
"type": "boolean | null",
|
|
9649
|
-
"optional": false
|
|
9650
|
+
"optional": false,
|
|
9651
|
+
"description": "True when this media item is a VIDEO, not an image. Product galleries, cards, swatches and\nline items must branch on it: render <video src={getDefaultSrc(item.image)}> instead of an\n<img>, otherwise the .mp4 URL lands in an <img> and the media breaks."
|
|
9650
9652
|
},
|
|
9651
9653
|
{
|
|
9652
9654
|
"name": "order",
|
|
@@ -14493,12 +14495,14 @@
|
|
|
14493
14495
|
{
|
|
14494
14496
|
"name": "name",
|
|
14495
14497
|
"type": "string",
|
|
14496
|
-
"optional": false
|
|
14498
|
+
"optional": false,
|
|
14499
|
+
"description": "Stable runtime key — the variable's `variableName` (e.g. `_6Q0KV7VGGM`)."
|
|
14497
14500
|
},
|
|
14498
14501
|
{
|
|
14499
14502
|
"name": "displayName",
|
|
14500
14503
|
"type": "string",
|
|
14501
|
-
"optional": false
|
|
14504
|
+
"optional": false,
|
|
14505
|
+
"description": "Human-facing label."
|
|
14502
14506
|
},
|
|
14503
14507
|
{
|
|
14504
14508
|
"name": "type",
|
|
@@ -14508,7 +14512,8 @@
|
|
|
14508
14512
|
{
|
|
14509
14513
|
"name": "value",
|
|
14510
14514
|
"type": "any",
|
|
14511
|
-
"optional": false
|
|
14515
|
+
"optional": false,
|
|
14516
|
+
"description": "Concrete value, discriminated by `type`:\n- TEXT/RICH_TEXT → string · NUMBER → number · BOOLEAN → boolean · COLOR → hex string.\n- IMAGE → an IkasImage REFERENCE, e.g. `{ id: \"theme-images/<uuid>\" }`. It has NO\n `.url` field — resolve to a URL with `getDefaultSrc(value)` / `getSrc(value, size)`\n from `@ikas/bp-storefront`, never read `value.url`.\n- BORDER/SHADOW → object, or `null` when unset."
|
|
14512
14517
|
}
|
|
14513
14518
|
],
|
|
14514
14519
|
"filePath": "theme-globals.ts"
|
|
@@ -14531,12 +14536,14 @@
|
|
|
14531
14536
|
{
|
|
14532
14537
|
"name": "resolved",
|
|
14533
14538
|
"type": "string | null",
|
|
14534
|
-
"optional": false
|
|
14539
|
+
"optional": false,
|
|
14540
|
+
"description": "Resolved base/default value (e.g. a hex string), or null when unset."
|
|
14535
14541
|
},
|
|
14536
14542
|
{
|
|
14537
14543
|
"name": "cssVar",
|
|
14538
14544
|
"type": "string",
|
|
14539
|
-
"optional": false
|
|
14545
|
+
"optional": false,
|
|
14546
|
+
"description": "CSS custom-property reference, e.g. `var(--primaryColor)`."
|
|
14540
14547
|
}
|
|
14541
14548
|
],
|
|
14542
14549
|
"filePath": "theme-globals.ts"
|
|
@@ -14559,17 +14566,20 @@
|
|
|
14559
14566
|
{
|
|
14560
14567
|
"name": "resolved",
|
|
14561
14568
|
"type": "Record<string, string>",
|
|
14562
|
-
"optional": false
|
|
14569
|
+
"optional": false,
|
|
14570
|
+
"description": "Resolved CSS values keyed by camelCased CSS property (e.g. `fontSize`, `fontWeight`)."
|
|
14563
14571
|
},
|
|
14564
14572
|
{
|
|
14565
14573
|
"name": "className",
|
|
14566
14574
|
"type": "string",
|
|
14567
|
-
"optional": false
|
|
14575
|
+
"optional": false,
|
|
14576
|
+
"description": "Class selector the editor emits for this text style, e.g. `_<id>`."
|
|
14568
14577
|
},
|
|
14569
14578
|
{
|
|
14570
14579
|
"name": "isDefault",
|
|
14571
14580
|
"type": "boolean",
|
|
14572
|
-
"optional": false
|
|
14581
|
+
"optional": false,
|
|
14582
|
+
"description": "True when this is the theme's default/global text style (the editor's \"Make Default Style\")."
|
|
14573
14583
|
}
|
|
14574
14584
|
],
|
|
14575
14585
|
"filePath": "theme-globals.ts"
|
|
@@ -14638,17 +14648,20 @@
|
|
|
14638
14648
|
{
|
|
14639
14649
|
"name": "ref",
|
|
14640
14650
|
"type": "string",
|
|
14641
|
-
"optional": false
|
|
14651
|
+
"optional": false,
|
|
14652
|
+
"description": "CSS reference — the animation name / class the editor emits, e.g. `_<id>`."
|
|
14642
14653
|
},
|
|
14643
14654
|
{
|
|
14644
14655
|
"name": "settings",
|
|
14645
14656
|
"type": "StyleEntry[]",
|
|
14646
|
-
"optional": true
|
|
14657
|
+
"optional": true,
|
|
14658
|
+
"description": "Keyframe-level animation \"settings\" (the editor's Settings popover). These are CSS\nanimation/transition properties and DIFFER BY TYPE: a `keyframe` carries\nanimation-duration/-iteration-count/-play-state/-delay/-timing-function/-direction/\n-fill-mode/-timeline/-range + transform-origin; a `transition` carries `transition`.\nBoth may carry backface-visibility. Empty when none are set."
|
|
14647
14659
|
},
|
|
14648
14660
|
{
|
|
14649
14661
|
"name": "points",
|
|
14650
14662
|
"type": "{ point: string; styles?: StyleEntry[] }[]",
|
|
14651
|
-
"optional": true
|
|
14663
|
+
"optional": true,
|
|
14664
|
+
"description": "Animation points; each point may carry its own resolved styles."
|
|
14652
14665
|
}
|
|
14653
14666
|
],
|
|
14654
14667
|
"filePath": "theme-globals.ts"
|
|
@@ -14694,12 +14707,14 @@
|
|
|
14694
14707
|
{
|
|
14695
14708
|
"name": "className",
|
|
14696
14709
|
"type": "string",
|
|
14697
|
-
"optional": false
|
|
14710
|
+
"optional": false,
|
|
14711
|
+
"description": "Class selector to activate this scheme value, e.g. `_<id>`."
|
|
14698
14712
|
},
|
|
14699
14713
|
{
|
|
14700
14714
|
"name": "colorsByScheme",
|
|
14701
14715
|
"type": "Record<string, { resolved: string | null; cssVar: string }>",
|
|
14702
|
-
"optional": false
|
|
14716
|
+
"optional": false,
|
|
14717
|
+
"description": "This palette's colors, keyed by color-SLOT id (the ids in\n`getThemeColorSchemes().schemes`). THIS — not the top-level `schemes` array — is\nthe source of truth for swatch colors; iterate it to render a palette. Prefer\n`cssVar` for live, in-editor reactivity; `resolved` is a render-time snapshot."
|
|
14703
14718
|
}
|
|
14704
14719
|
],
|
|
14705
14720
|
"filePath": "theme-globals.ts"
|
|
@@ -14929,7 +14944,8 @@
|
|
|
14929
14944
|
{
|
|
14930
14945
|
"name": "designAssetId",
|
|
14931
14946
|
"type": "string",
|
|
14932
|
-
"optional": true
|
|
14947
|
+
"optional": true,
|
|
14948
|
+
"description": "Set when this scheme belongs to an installed design asset; groups the default fallback."
|
|
14933
14949
|
},
|
|
14934
14950
|
{
|
|
14935
14951
|
"name": "colors",
|
package/dist/index.js
CHANGED
|
@@ -1828,6 +1828,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1828
1828
|
if (td.properties) {
|
|
1829
1829
|
for (const p of td.properties) {
|
|
1830
1830
|
const opt = p.optional ? "?" : "";
|
|
1831
|
+
if (p.description) {
|
|
1832
|
+
lines.push(" /**", ...p.description.split("\n").map((l) => ` * ${l.trim()}`), " */");
|
|
1833
|
+
}
|
|
1831
1834
|
lines.push(` ${p.name}${opt}: ${p.type};`);
|
|
1832
1835
|
}
|
|
1833
1836
|
}
|
|
@@ -1890,7 +1893,7 @@ const server = new McpServer({
|
|
|
1890
1893
|
version: "0.1.0",
|
|
1891
1894
|
}, {
|
|
1892
1895
|
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
|
|
1893
|
-
"Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
|
|
1896
|
+
"Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `move_page_section` → reorder a section already placed on a page (by `direction` up/down or an absolute `to_index`) — the only way to change section order, since the editor UI offers it solely as a drag; `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
|
|
1894
1897
|
"To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
|
|
1895
1898
|
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1896
1899
|
"(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
|
|
@@ -3780,6 +3783,42 @@ server.tool("list_page_sections", "List the sections placed on a page — a LEAN
|
|
|
3780
3783
|
];
|
|
3781
3784
|
return callEditorAction(project_root, args);
|
|
3782
3785
|
});
|
|
3786
|
+
// Tool: move_page_section
|
|
3787
|
+
server.tool("move_page_section", "Reorder a section that is ALREADY placed on a page — the only way to change section order, since the editor UI offers it solely as a drag in the layers tree. Target the placement by its `element_id` from `list_page_sections` (NOT the sectionId/componentId — a page can hold several placements of the same section and each moves independently). Give EITHER `direction` (\"up\"/\"down\", one position — use this for a simple nudge) OR `to_index` (absolute 0-based position), never both. Indices are 0-based and count ONLY placed sections, in exactly the order `list_page_sections` returns them, so you can read that list, pick a target position from it, and pass it straight back. The result returns `fromIndex`, `resultIndex` (where the section ACTUALLY ended up) and `sections` (the full resulting order) — assert on `resultIndex`/`sections` rather than assuming the move landed where you asked; if it lands somewhere other than requested a `note` says so. A reorder that does not take effect comes back as an ERROR, never as a success that changed nothing, so a successful result means the section really moved. Out-of-range indices are rejected with the valid range (and the section's current index) instead of being clamped. Passing a `sectionId`/`componentId` where an `element_id` belongs is detected and the error names the elementId you actually wanted. COMMON (global) sections — the theme's header and footer — are REJECTED: their position is fixed by the theme, so there is nothing to reorder. To place a NEW section at a specific position use `add_sections_to_page` with its `index` instead; this tool only moves existing placements.", {
|
|
3788
|
+
project_root: z
|
|
3789
|
+
.string()
|
|
3790
|
+
.describe("Absolute path to the code-component project."),
|
|
3791
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3792
|
+
element_id: z
|
|
3793
|
+
.string()
|
|
3794
|
+
.describe("Placed-section elementId to move (from `list_page_sections`)."),
|
|
3795
|
+
direction: z
|
|
3796
|
+
.enum(["up", "down"])
|
|
3797
|
+
.optional()
|
|
3798
|
+
.describe("Move one position up or down. Provide this or `to_index`, not both."),
|
|
3799
|
+
to_index: z
|
|
3800
|
+
.number()
|
|
3801
|
+
.int()
|
|
3802
|
+
.min(0)
|
|
3803
|
+
.optional()
|
|
3804
|
+
.describe("Absolute 0-based target position among the page's placed sections. Provide this or `direction`, not both."),
|
|
3805
|
+
port: z
|
|
3806
|
+
.number()
|
|
3807
|
+
.optional()
|
|
3808
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3809
|
+
}, async ({ project_root, page_id, element_id, direction, to_index, port }) => {
|
|
3810
|
+
const args = [
|
|
3811
|
+
"move-page-section",
|
|
3812
|
+
"--page-id",
|
|
3813
|
+
page_id,
|
|
3814
|
+
"--element-id",
|
|
3815
|
+
element_id,
|
|
3816
|
+
...(direction ? ["--direction", direction] : []),
|
|
3817
|
+
...(to_index !== undefined ? ["--to-index", String(to_index)] : []),
|
|
3818
|
+
...(port ? ["--port", String(port)] : []),
|
|
3819
|
+
];
|
|
3820
|
+
return callEditorAction(project_root, args);
|
|
3821
|
+
});
|
|
3783
3822
|
// Tool: get_component_props
|
|
3784
3823
|
server.tool("get_component_props", "Get prop blueprints straight from the editor (the source of truth) for one or MANY components in a single call — for any section OR child code component ids. Returns `{ components: [...] }`, each prop carrying its `id`, `name`, `type`, `required`, `default`, and crucially: a ready-to-use `writeExample` (the EXACT value shape to pass to update_section_prop / update_page_sections — e.g. PRODUCT_LIST, BLOG_LIST, COMPONENT_LIST, LINK) plus a `writeNote` for variants, so you do NOT have to guess or trial-write the JSON; for ENUM props the `options` (valid `value`s); and for COMPONENT/COMPONENT_LIST props the `allowedComponentIds` (which children the slot permits — when empty, an `allowedComponentsNote` says any imported code component is allowed). Use this (its `writeExample`/`options`/`allowedComponentIds`) instead of guessing or reading ikas.config.json. Pass ALL the section/child ids you need at once (batch). Child component ids come from a parent prop's `allowedComponentIds` (or get_section_template).", {
|
|
3785
3824
|
project_root: z
|
|
@@ -4227,6 +4266,7 @@ const EDITOR_WORKFLOW_GUIDE = [
|
|
|
4227
4266
|
"## Building a whole page fast (placing + filling)",
|
|
4228
4267
|
"- add_sections_to_page(page_id, sections:[{component_id, index?, updates:[{prop_name,value}]}]) places MANY sections AND sets their props in ONE call, returning each new elementId. This is the fastest path — prefer it over add_section_to_page per section followed by separate fills. Built-but-unimported section/child components are auto-imported.",
|
|
4229
4268
|
"- For a single section: list_imported_sections (confirm imported; else import_section, after building) → add_section_to_page(component_id, page_id, index?) → then list_page_sections to get its elementId.",
|
|
4269
|
+
"- REORDERING an already-placed section is a different tool: move_page_section(page_id, element_id, direction:'up'|'down' | to_index:<n>). The `index` on add_* only positions a NEW placement; it cannot move an existing one, and the editor UI exposes reordering solely as a drag, so this tool is the only way. Indices are 0-based over the placed sections in list_page_sections order; the result returns `resultIndex` + the full resulting `sections` order — assert on those. Header/footer (COMMON) sections cannot be reordered and are rejected.",
|
|
4230
4270
|
"",
|
|
4231
4271
|
"## Value shapes by prop type (what to pass as `value`)",
|
|
4232
4272
|
'- Scalars are WRAPPED: TEXT/RICH_TEXT { "value": "..." }, BOOLEAN { "value": true }, NUMBER { "value": 12 }, COLOR { "value": "#FF0000" }, ENUM { "value": "<key>" }, DATE { "value": ... }.',
|