@ikas/code-components-mcp 1.4.0-beta.6 → 1.4.0-beta.7
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 +34 -34
- package/data/migration.json +33 -19
- package/data/storefront-api.json +1 -1
- package/data/storefront-types.json +1 -1
- package/dist/index.js +22 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/migration.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"generatedAt": "2026-04-13T00:00:00.000Z",
|
|
3
3
|
"topics": {
|
|
4
4
|
"migration-overview": {
|
|
5
|
-
"title": "Old Theme
|
|
5
|
+
"title": "Old Theme \u2192 Code Component Migration Overview",
|
|
6
6
|
"description": "End-to-end workflow for converting an old ikas storefront theme into a new code-component project",
|
|
7
|
-
"content": "This guide covers converting themes built with the old ikas storefront system (React, theme.json, @ikas/storefront, Tailwind CSS) into the new code-component system (Preact, ikas.config.json, @ikas/bp-storefront, scoped CSS).\n\n## FIRST: Choose a Workflow\n\n### For themes with >5 sections: USE THE ITERATIVE WORKFLOW\n\nLarge themes cannot be migrated in a single LLM session. Use the four-phase resumable workflow:\n\n1. **Phase A (once):** Call `plan_migration(theme_json, old_source_dir)`
|
|
7
|
+
"content": "This guide covers converting themes built with the old ikas storefront system (React, theme.json, @ikas/storefront, Tailwind CSS) into the new code-component system (Preact, ikas.config.json, @ikas/bp-storefront, scoped CSS).\n\n## FIRST: Choose a Workflow\n\n### For themes with >5 sections: USE THE ITERATIVE WORKFLOW\n\nLarge themes cannot be migrated in a single LLM session. Use the four-phase resumable workflow:\n\n1. **Phase A (once):** Call `plan_migration(theme_json, old_source_dir)` \u2014 generates MIGRATION.md with extracted CSS vars, enums, detected shared sub-components, and an ordered section queue with canonical component IDs.\n2. **Phase B (once):** Execute the foundation checklist \u2014 create global.css, run enum CLI commands, build shared sub-components.\n3. **Phase C (loop):** For each section, call `get_section_migration_plan(theme_json, section_name)` \u2192 consult `get_migration_guide(\"custom-data-conversion\")` and `get_migration_guide(\"component-composition-decision-guide\")` for prop-shape choices \u2192 run CLI commands \u2192 write `index.tsx` and `styles.css` \u2192 mark `[x]` in MIGRATION.md.\n4. **Phase D (any session):** First read MIGRATION.md, find the first `[ ]` item, resume.\n\n**See `get_migration_guide(\"iterative-workflow\")` for the full protocol.**\n\n### For small themes (< 5 sections): ONE-PASS WORKFLOW\n\n1. Call `analyze_old_theme` to understand the structure\n2. Generate ikas.config.json and all component files in one pass\n3. Build and verify\n\n## MCP Responsibilities vs LLM Responsibilities\n\nA successful migration depends on understanding what the MCP can and cannot do for you:\n\n- **MCP writes:** The initial `MIGRATION.md` (once, when you call `plan_migration` with `project_root`). Never again.\n- **MCP provides:** Reference data via read-only tools \u2014 section templates, type docs, framework guides, per-section migration plans.\n- **LLM owns:** All ongoing edits to `MIGRATION.md` \u2014 checkboxes, custom-data decisions, source-code-analysis findings, notes. Use your standard file-editing tools.\n- **LLM must do:** Source code analysis. `theme.json` does NOT list atomic UI primitives (Button, Input, Card, icons, layout helpers). These live only in old `src/` and the MCP cannot see them. You scan; you log them in MIGRATION.md.\n- **LLM decides:** Every customData enum-vs-component choice. The MCP's auto-classification (flat scalars \u2192 enum, structured records \u2192 component) is a default \u2014 verify each one against the actual data semantics.\n\nThere is no `resume_migration`, `mark_section_complete`, or `record_custom_data_decision` tool. By design, the LLM uses its own Read/Edit tools to update MIGRATION.md. The MCP's job is to give you a high-quality starting scaffold and ongoing reference data.\n\n## CRITICAL: The Two Systems Are Completely Different\n\n**The old storefront system (`@ikas/storefront`) and the new code-component system (`@ikas/bp-storefront`) are entirely separate systems with different packages, different data types, different APIs, and different runtime behaviors.** Even when type names look the same (e.g., `IkasImage`, `IkasProduct`, `IkasNavigationLink`), they are **different types from different packages** with potentially different properties and methods.\n\n**NEVER assume** that because a type exists in the old system, it works the same way in the new system:\n- `IkasImage` from `@ikas/storefront` \u2260 `IkasImage` from `@ikas/bp-storefront` \u2014 different properties, different helper functions\n- `IkasProduct` from `@ikas/storefront` \u2260 `IkasProduct` from `@ikas/bp-storefront` \u2014 different data shape\n- `IkasNavigationLink` from `@ikas/storefront` \u2260 `IkasNavigationLink` from `@ikas/bp-storefront` \u2014 different structure\n- `IkasSlider` from `@ikas/storefront` \u2192 **does not exist** in the new system (replaced by plain `number`)\n- The old `<Image>` component \u2192 **does not exist** in the new system (use `getDefaultSrc()` + native `<img>`)\n- The old `useStore()` hook \u2192 **does not exist** in the new system (import stores directly)\n- The old `observer()` from `mobx-react-lite` \u2192 **must not be used** on root components (use `observer` from `@ikas/component-utils` only on sub-components)\n\n**Similarly, the prop type systems are completely different:**\n- Old system prop types (in theme.json) are defined by `IkasThemeJsonComponentPropType` enum \u2014 a different set of strings with different semantics\n- New system prop types (in ikas.config.json) are defined by the code-component `PropType` enum\n- Prop types with the same name (e.g., `TEXT`, `IMAGE`, `BOOLEAN`) may seem identical but produce **different TypeScript types** and have **different editor UI behaviors**\n- The old system has types that don't exist in the new system (`SLIDER`, `CUSTOM`, `PRODUCT_DETAIL`)\n- The new system has types that don't exist in the old system (`NUMBER`, `NUMBER_RANGE`, `DATE`, `TYPE`, `FUNCTION`, `COMPONENT`)\n\n**Always use the MCP server's `get_prop_types`, `get_type_definition`, `get_function_doc`, and `get_model_guide` tools** to look up the correct new-system types, functions, and APIs. Never copy old-system type usage into new-system code.\n\n## Key Differences Summary\n\n| Old System | New System |\n|-----------|------------|\n| React 18 | Preact 10 |\n| @ikas/storefront | @ikas/bp-storefront |\n| theme.json | ikas.config.json |\n| Tailwind CSS | Scoped plain CSS |\n| External libraries allowed | No external libraries |\n| CUSTOM + customData | COMPONENT_LIST + child components |\n| SLIDER prop (IkasSlider) | NUMBER prop (number) |\n| PRODUCT_DETAIL | PRODUCT |\n| observer() on all components | observer() only on sub-components |\n| CSS Modules / Tailwind | .cc_{id} scoped CSS |\n| Next.js pages | Sections composed in editor |",
|
|
8
8
|
"tags": [
|
|
9
9
|
"migration",
|
|
10
10
|
"overview",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
17
|
"prop-type-mapping": {
|
|
18
|
-
"title": "Prop Type Mapping: Old
|
|
18
|
+
"title": "Prop Type Mapping: Old \u2192 New",
|
|
19
19
|
"description": "Complete mapping of old theme.json prop types to new ikas.config.json prop types",
|
|
20
|
-
"content": "## IMPORTANT: Different Systems, Different Types\n\n**The old prop type system (theme.json) and the new prop type system (ikas.config.json) are completely separate.** Even when a prop type has the same name in both systems (e.g., `IMAGE`, `LINK`, `PRODUCT_LIST`), the underlying TypeScript types come from different packages (`@ikas/storefront` vs `@ikas/bp-storefront`) and may have different properties, methods, and nullability. Always use the MCP tools (`get_type_definition`, `get_model_guide`) to look up the exact new-system types
|
|
20
|
+
"content": "## IMPORTANT: Different Systems, Different Types\n\n**The old prop type system (theme.json) and the new prop type system (ikas.config.json) are completely separate.** Even when a prop type has the same name in both systems (e.g., `IMAGE`, `LINK`, `PRODUCT_LIST`), the underlying TypeScript types come from different packages (`@ikas/storefront` vs `@ikas/bp-storefront`) and may have different properties, methods, and nullability. Always use the MCP tools (`get_type_definition`, `get_model_guide`) to look up the exact new-system types \u2014 never assume old-system knowledge applies.\n\n## Direct Mappings (1:1)\n\nThese prop types have similar equivalents, but remember the TypeScript types are from different packages:\n\n| Old Type | New Type | TypeScript (Old) | TypeScript (New) | Notes |\n|----------|----------|-----------------|-----------------|-------|\n| `TEXT` | `TEXT` | `string` | `string` | |\n| `RICH_TEXT` | `RICH_TEXT` | `string` | `string` | |\n| `BOOLEAN` | `BOOLEAN` | `boolean` | `boolean` | |\n| `IMAGE` | `IMAGE` | `IkasImage` | `IkasImage \\| null` | New type is nullable |\n| `IMAGE_LIST` | `IMAGE_LIST` | `IkasImage[]` | `IkasImageList` | Slight type difference |\n| `LINK` | `LINK` | `IkasNavigationLink` | `IkasNavigationLink` | |\n| `LIST_OF_LINK` | `LIST_OF_LINK` | `IkasNavigationLink[]` | `IkasNavigationLinkList` | Slight type difference |\n| `COLOR` | `COLOR` | `string` | `string` | |\n| `VIDEO` | `VIDEO` | `IkasVideo` | `IkasVideo \\| null` | New type is nullable |\n| `PRODUCT_LIST` | `PRODUCT_LIST` | `IkasProductList` | `IkasProductList` | |\n| `CATEGORY` | `CATEGORY` | `IkasCategory` | `IkasCategory \\| null` | |\n| `CATEGORY_LIST` | `CATEGORY_LIST` | `IkasCategoryList` | `IkasCategoryList` | |\n| `BRAND` | `BRAND` | `IkasBrand` | `IkasBrand \\| null` | |\n| `BRAND_LIST` | `BRAND_LIST` | \u2014 | `IkasBrandList` | |\n| `BLOG` | `BLOG` | `IkasBlog` | `IkasBlog \\| null` | |\n| `BLOG_LIST` | `BLOG_LIST` | `IkasBlogList` | `IkasBlogList` | |\n| `BLOG_CATEGORY` | `BLOG_CATEGORY` | `IkasBlogCategory` | `IkasBlogCategory \\| null` | |\n| `BLOG_CATEGORY_LIST` | `BLOG_CATEGORY_LIST` | `IkasBlogCategoryList` | `IkasBlogCategoryList` | |\n| `PRODUCT_ATTRIBUTE` | `PRODUCT_ATTRIBUTE` | `IkasAttributeDetail` | `IkasProductAttributeValue \\| null` | Type renamed |\n| `PRODUCT_ATTRIBUTE_LIST` | `PRODUCT_ATTRIBUTE_LIST` | `IkasAttributeList` | `IkasProductAttributeValue[]` | Type renamed |\n| `COMPONENT_LIST` | `COMPONENT_LIST` | `IkasComponentRenderer` | `any` (rendered via `<IkasComponentRenderer>`) | |\n\n## Renamed Types\n\n| Old Type | New Type | Notes |\n|----------|----------|-------|\n| `PRODUCT_DETAIL` | `PRODUCT` | Same concept, just renamed. Old: `IkasProduct`, New: `IkasProduct \\| null` |\n\n## Types Requiring Conversion\n\n### SLIDER \u2192 NUMBER\nOld `SLIDER` type used `IkasSlider` with `{ value: number }`. The new system uses `NUMBER` which maps to plain `number`.\n\n**Old theme.json:**\n```json\n{\n \"name\": \"logoWidth\",\n \"type\": \"SLIDER\",\n \"sliderData\": { \"min\": 80, \"max\": 300, \"interval\": 1 }\n}\n```\n\n**New ikas.config.json:**\n```json\n{\n \"name\": \"logoWidth\",\n \"displayName\": \"Logo Width\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"defaultValue\": 120\n}\n```\n\n**Code change:** Replace `logoWidth?.value` or `logoWidth.value` with just `logoWidth` (it's already a number).\n\n### CUSTOM \u2192 Multiple approaches\nSee `get_migration_guide(\"custom-data-conversion\")` for the full guide. Summary:\n\n| Old customData type | New approach |\n|--------------------|--------------|\n| DYNAMIC_LIST (list of objects) | `COMPONENT_LIST` + child component |\n| OBJECT (\u22643 simple fields) | Flatten into direct props on parent |\n| OBJECT (>3 fields or complex) | `COMPONENT_LIST` + child component |\n| STATIC_LIST (fixed count items) | `COMPONENT_LIST` (store owner manages count) |\n| ENUM (enumOptions) | `ENUM` with custom enum via `config add-enum` |\n\n### ENUM \u2192 ENUM\nOld ENUM type with `enumOptions` maps to new ENUM with `enumTypeId`. Create custom enums first:\n```bash\nnpx ikas-component config add-enum --name \"AspectRatio\" --options '{\"Square\":\"1:1\",\"Landscape\":\"16:9\",\"Portrait\":\"3:4\"}'\n# Use returned enumId in prop definition\n```\n\n## Types Not Available in New System\n\nThese old types do not exist in the new system and need workarounds:\n\n| Old Type | Workaround |\n|----------|------------|\n| `SLIDER` | Use `NUMBER`. Access value directly instead of `.value` |\n| `OBJECT` (customData) | Flatten into parent props or use COMPONENT_LIST |\n| `STATIC_LIST` (customData) | Use COMPONENT_LIST |\n\n## New Types Not in Old System\n\n| New Type | Description |\n|----------|-------------|\n| `NUMBER` | Numeric input (replaces SLIDER) |\n| `NUMBER_RANGE` | Two-number range (IkasNumberRange) |\n| `DATE` | Date picker |\n| `FUNCTION` | Function prop (callback) |\n| `TYPE` | Structured style type (padding, margin, etc.) |\n| `COMPONENT` | Single child component slot |\n| `RAFFLE` / `RAFFLE_LIST` | Raffle references |",
|
|
21
21
|
"tags": [
|
|
22
22
|
"migration",
|
|
23
23
|
"props",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"custom-data-conversion": {
|
|
33
33
|
"title": "Converting CUSTOM Props and customData",
|
|
34
34
|
"description": "How to convert old CUSTOM prop types with customData definitions to new COMPONENT_LIST + child components",
|
|
35
|
-
"content": "The old system's `CUSTOM` prop type + `customData` definitions are the most complex part of any migration. Each old customData entry takes one of two paths in the new system
|
|
35
|
+
"content": "The old system's `CUSTOM` prop type + `customData` definitions are the most complex part of any migration. Each old customData entry takes one of two paths in the new system \u2014 and the path is NOT a 1:1 mechanical conversion. **You must decide per type.**\n\n## The Feature-Parity Rule\n\nMigration is a port, not a redesign. If a customData type carries richer structure than the closest built-in prop type can hold, the answer is a new component \u2014 not flattening. Phrases like \"I'll add it later via COMPONENT_LIST\" are a red flag; the data is structured for a reason, and the cleanest time to model that structure is at migration time. Get explicit user approval before dropping any feature, and log the approval in `MIGRATION.md` \u2192 `## Notes`.\n\nThis is the rule the rest of this guide operationalizes.\n\n## The Heuristic (Short Version)\n\n- Flat scalar set (e.g. `\"left\" | \"right\" | \"center\"`) \u2192 enum prop via `config add-enum`.\n- Structured record with \u22653 fields \u2192 component via `config add-component` + `COMPONENT_LIST` on the parent.\n- Structured record with 1\u20132 fields \u2192 usually flatten to scalar props on the parent (and log the decision).\n\nFor the full decision tree (including domain LIST types like `LIST_OF_LINK`/`IMAGE_LIST` and the BOOLEAN-toggle anti-pattern), see `get_migration_guide(\"component-composition-decision-guide\")`.\n\n## When you call `get_section_migration_plan`\n\nThe MCP scans each prop and emits a **\"Custom Data Decisions to Make\"** callout for every prop that references a customData type. The callout shows the shape and the MCP's default classification. **Verify each default against the actual data semantics** \u2014 the MCP only sees shape, not usage. Log every final decision in `MIGRATION.md` under `## Custom Data Decisions`.\n\n## Worked Example 1: `Position` \u2192 enum prop\n\nA classic enum case. The old customData is a flat scalar set.\n\n### Old System\n\n```json\n// customData entry:\n{\n \"id\": \"...\",\n \"name\": \"Position\",\n \"type\": \"ENUM\",\n \"enumOptions\": [\n { \"displayName\": \"Left\", \"value\": \"left\" },\n { \"displayName\": \"Right\", \"value\": \"right\" },\n { \"displayName\": \"Center\", \"value\": \"center\" }\n ]\n}\n// Component prop:\n{ \"name\": \"alignment\", \"type\": \"CUSTOM\", \"customDataId\": \"...\" }\n```\n\n### New System (correct path: enum prop)\n\n```bash\nnpx ikas-component config add-enum --name \"Position\" --options '{\"Left\":\"left\",\"Right\":\"right\",\"Center\":\"center\"}'\n# \u2192 { \"success\": true, \"enumId\": \"enm_abc123\" }\n```\n\nThen on the parent section:\n\n```bash\nnpx ikas-component config add-prop --component <section-id> --name \"alignment\" --type ENUM --enumTypeId enm_abc123\n```\n\nIn TSX:\n\n```tsx\n<div style={{ textAlign: alignment }}>...</div>\n// `alignment` is a string with value \"left\" | \"right\" | \"center\"\n```\n\n## Worked Example 2: `Slide` \u2192 component + COMPONENT_LIST\n\nA repeating structured record. Multiple fields per item, used as a list.\n\n### Old System\n\n```json\n// customData entry:\n{\n \"name\": \"Slide\",\n \"type\": \"DYNAMIC_LIST\",\n \"nestedData\": [{\n \"type\": \"OBJECT\",\n \"nestedData\": [\n { \"key\": \"image\", \"type\": \"IMAGE\", \"name\": \"Image\" },\n { \"key\": \"title\", \"type\": \"TEXT\", \"name\": \"Title\" },\n { \"key\": \"link\", \"type\": \"LINK\", \"name\": \"Link\" }\n ]\n }]\n}\n// HeroSlider prop:\n{ \"name\": \"slides\", \"type\": \"CUSTOM\", \"customDataId\": \"...\" }\n```\n\n### New System (correct path: component + COMPONENT_LIST)\n\n```bash\n# 1. Create the Slide component\nnpx ikas-component config add-component --name \"Slide\" --type component \\\n --props '[{\"name\":\"image\",\"displayName\":\"Image\",\"type\":\"IMAGE\"},{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\"},{\"name\":\"link\",\"displayName\":\"Link\",\"type\":\"LINK\"}]'\n# \u2192 { \"componentId\": \"cc_slide_xyz\" }\n\n# 2. Wire it into HeroSlider via COMPONENT_LIST + filteredComponentIds\nnpx ikas-component config add-component --name \"HeroSlider\" --type section \\\n --props '[{\"name\":\"slides\",\"displayName\":\"Slides\",\"type\":\"COMPONENT_LIST\",\"filteredComponentIds\":[\"cc_slide_xyz\"]}]'\n```\n\nIn TSX (HeroSlider renders Slide instances via IkasComponentRenderer):\n\n```tsx\n<IkasComponentRenderer id=\"slides\" components={slides as any[]} parentProps={props} />\n```\n\nIn TSX (Slide is a self-contained component with its three props):\n\n```tsx\nexport default function Slide({ image, title, link }: Props) {\n return (\n <a href={link?.href}>\n {image && <img src={getDefaultSrc(image)} alt={title} />}\n <h3>{title}</h3>\n </a>\n );\n}\n```\n\n## Worked Example 3: `MenuItem` (mega-menu) \u2192 component, NOT LIST_OF_LINK\n\nWhen the old theme has navigation links with **per-link metadata** (image, color, icon, submenu layout columns), `LIST_OF_LINK` cannot represent it. `IkasNavigationLink` only has `href`, `label`, `subLinks`, `openInNewTab`, `itemId`, `type` \u2014 there is **no slot for an image or any custom field**.\n\nThe correct conversion is the same pattern as `Slide`: build a dedicated component (`MenuItem`) with the per-link fields, then render an array of them via COMPONENT_LIST on the parent header/navbar.\n\n### Old System\n\n```json\n// customData entry:\n{\n \"name\": \"MenuItem\",\n \"type\": \"DYNAMIC_LIST\",\n \"nestedData\": [{\n \"type\": \"OBJECT\",\n \"nestedData\": [\n { \"key\": \"image\", \"type\": \"IMAGE\", \"name\": \"Image\" },\n { \"key\": \"title\", \"type\": \"TEXT\", \"name\": \"Title\" },\n { \"key\": \"link\", \"type\": \"LINK\", \"name\": \"Link\" }\n ]\n }]\n}\n// Header prop:\n{ \"name\": \"menuItems\", \"type\": \"CUSTOM\", \"customDataId\": \"...\" }\n```\n\n### New System (correct path: MenuItem component)\n\n```bash\nnpx ikas-component config add-component --name \"MenuItem\" --type component \\\n --props '[{\"name\":\"image\",\"displayName\":\"Image\",\"type\":\"IMAGE\"},{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\"},{\"name\":\"link\",\"displayName\":\"Link\",\"type\":\"LINK\"}]'\n# \u2192 { \"componentId\": \"cc_menu_item_xyz\" }\n```\n\nThen wire into the Header section as a `COMPONENT_LIST` prop with `filteredComponentIds: [\"cc_menu_item_xyz\"]`.\n\n### Why NOT `LIST_OF_LINK`?\n\n```tsx\n// IkasNavigationLink \u2014 what LIST_OF_LINK gives you:\ntype IkasNavigationLink = {\n href: string;\n label: string;\n subLinks: IkasNavigationLink[];\n itemId: string;\n type: string;\n openInNewTab: boolean;\n};\n```\n\n**There is no `image` field.** If you flatten a mega-menu into `LIST_OF_LINK` you silently drop the image, color, custom icon, column count, and every other non-link field. Always prefer a `MenuItem`-style component when the old data is richer than href + label.\n\n## Supporting Cases (variations on the same patterns)\n\n### Simple OBJECT with \u22643 simple fields \u2192 flatten into parent props\n\nWhen a CUSTOM prop references an OBJECT with just 2-3 simple fields and is NOT used as a list, flattening into the parent's direct props is cleaner than building a child component:\n\n```json\n// Old customData OBJECT: { title: TEXT, color: COLOR }\n// Old prop: { \"name\": \"textData\", \"type\": \"CUSTOM\", \"customDataId\": \"...\" }\n\n// New: just expose two direct props on the parent\n[\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\" },\n { \"name\": \"titleColor\", \"displayName\": \"Title Color\", \"type\": \"COLOR\" }\n]\n```\n\nThis is the **only** place where customData becomes neither an enum nor a component \u2014 purely because the old wrapping was an unnecessary abstraction.\n\n### Nested DYNAMIC_LIST inside an OBJECT \u2192 nested components\n\nE.g. a NavLink with `mainLink` (LINK) + `subLinks` (DYNAMIC_LIST of SubLink). Build a component per level (NavLink, SubLink), each with its own props, and nest COMPONENT_LIST slots.\n\n### STATIC_LIST \u2192 still a component\n\n`STATIC_LIST` differs from `DYNAMIC_LIST` only in editor UX (fixed count vs add/remove). In the new system, both are COMPONENT_LIST with a child component.\n\n## Rendering Lists \u2014 COMPONENT_LIST vs Data Props\n\nFor the COMPONENT_LIST (merchant hand-picks items) vs data-prop (`PRODUCT_LIST` / `BLOG_LIST` / `CATEGORY_LIST`, data-driven) distinction, see `get_framework_guide(\"component-renderer-patterns\")`. The same registered child component (e.g. `ProductCard`) is reusable across both patterns.\n\n## Naming Convention for Child Components\n\n- Parent section: `{OldComponentName}Section` (e.g., `StoresSection`, `HeroSliderSection`)\n- Child component: `{ItemTypescriptName}` or `{ParentName}{ItemName}` (e.g., `StoreCard`, `Slide`, `MenuItem`)\n- Use `filteredComponentIds` on the COMPONENT_LIST prop to restrict which child components can be placed there\n\n## Logging Your Decision\n\nAfter every customData decision, append a row to `MIGRATION.md` under `## Custom Data Decisions`:\n\n```\n- `SlideData` \u2192 component `slide` (2026-05-11) \u2014 structured record {image, link, title}; wired into HeroSlider via COMPONENT_LIST.\n- `Position` \u2192 enum `Position` (2026-05-11) \u2014 flat scalar set left/right/center; enumId: enm_abc123.\n- `MenuItemData` \u2192 component `menu-item` (2026-05-12) \u2014 mega-menu links with image + title; LIST_OF_LINK would have dropped the image.\n```\n\nThis is how future sessions (and you, later) know which path was taken and avoid re-deciding inconsistently.\n",
|
|
36
36
|
"tags": [
|
|
37
37
|
"migration",
|
|
38
38
|
"CUSTOM",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"library-replacements": {
|
|
49
49
|
"title": "Library Replacement Patterns",
|
|
50
50
|
"description": "How to replace common third-party libraries with vanilla Preact + CSS implementations",
|
|
51
|
-
"content": "The new code-component system does not support external libraries. Here's how to replace each common library found in old themes.\n\n## swiper → CSS Scroll-Snap Carousel\n\n**What it does:** Touch-enabled carousel/slider with autoplay, pagination, navigation.\n\n**Replacement pattern:**\n```tsx\nimport { useRef, useState, useEffect } from \"preact/hooks\";\n\nfunction Carousel({ children }: { children: any[] }) {\n const trackRef = useRef<HTMLDivElement>(null);\n const [current, setCurrent] = useState(0);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n const handleScroll = () => {\n const index = Math.round(track.scrollLeft / track.clientWidth);\n setCurrent(index);\n };\n track.addEventListener(\"scroll\", handleScroll, { passive: true });\n return () => track.removeEventListener(\"scroll\", handleScroll);\n }, []);\n\n const goTo = (index: number) => {\n trackRef.current?.scrollTo({ left: index * trackRef.current.clientWidth, behavior: \"smooth\" });\n };\n\n return (\n <div className=\"carousel\">\n <div ref={trackRef} className=\"carousel-track\">\n {children}\n </div>\n <div className=\"carousel-dots\">\n {children.map((_, i) => (\n <button\n key={i}\n className={`carousel-dot ${i === current ? \"active\" : \"\"}`}\n onClick={() => goTo(i)}\n />\n ))}\n </div>\n </div>\n );\n}\n```\n\n**CSS:**\n```css\n.carousel-track {\n display: flex;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n -webkit-overflow-scrolling: touch;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track > * {\n flex: 0 0 100%;\n scroll-snap-align: start;\n}\n.carousel-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: #ccc; border: none; cursor: pointer;\n}\n.carousel-dot.active { background: #333; }\n```\n\n**For autoplay**, add a `useEffect` with `setInterval` that calls `goTo((current + 1) % count)`.\n\n**For `slidesPerView: \"auto\"`**, remove `flex: 0 0 100%` and let children size themselves.\n\n## @headlessui/react → Custom Preact Components\n\n**Disclosure (accordion):**\n```tsx\nimport { useState } from \"preact/hooks\";\n\nfunction Disclosure({ title, children }: { title: string; children: any }) {\n const [open, setOpen] = useState(false);\n return (\n <div>\n <button onClick={() => setOpen(!open)} className=\"disclosure-btn\">\n {title}\n <span className={`arrow ${open ? \"open\" : \"\"}`}>▸</span>\n </button>\n {open && <div className=\"disclosure-panel\">{children}</div>}\n </div>\n );\n}\n```\n\n**Dialog (modal):**\n```tsx\nimport { useEffect, useRef } from \"preact/hooks\";\n\nfunction Dialog({ open, onClose, children }: { open: boolean; onClose: () => void; children: any }) {\n const overlayRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (open) document.body.style.overflow = \"hidden\";\n else document.body.style.overflow = \"\";\n return () => { document.body.style.overflow = \"\"; };\n }, [open]);\n\n if (!open) return null;\n return (\n <div ref={overlayRef} className=\"dialog-overlay\" onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}>\n <div className=\"dialog-content\">{children}</div>\n </div>\n );\n}\n```\n\n## recharts → SVG or CSS Charts\n\nFor simple bar/line charts, use inline SVG. For complex charts, consider CSS-based bar charts:\n```tsx\nfunction BarChart({ data }: { data: { label: string; value: number }[] }) {\n const max = Math.max(...data.map(d => d.value));\n return (\n <div className=\"bar-chart\">\n {data.map((d, i) => (\n <div key={i} className=\"bar-item\">\n <div className=\"bar\" style={{ height: `${(d.value / max) * 100}%` }} />\n <span className=\"bar-label\">{d.label}</span>\n </div>\n ))}\n </div>\n );\n}\n```\n\n## react-player → Native Video/Iframe\n\n```tsx\nfunction VideoPlayer({ url, poster }: { url: string; poster?: string }) {\n if (url.includes(\"youtube.com\") || url.includes(\"youtu.be\")) {\n const videoId = url.match(/(?:v=|youtu\\.be\\/)([^&]+)/)?.[1];\n return <iframe src={`https://www.youtube.com/embed/${videoId}`} allowFullScreen className=\"video-iframe\" />;\n }\n if (url.includes(\"vimeo.com\")) {\n const videoId = url.match(/vimeo\\.com\\/(\\d+)/)?.[1];\n return <iframe src={`https://player.vimeo.com/video/${videoId}`} allowFullScreen className=\"video-iframe\" />;\n }\n return <video src={url} poster={poster} controls className=\"video-native\" />;\n}\n```\n\n## react-simple-star-rating → CSS Star Rating\n\n```tsx\nfunction StarRating({ rating, max = 5 }: { rating: number; max?: number }) {\n return (\n <div className=\"star-rating\">\n {Array.from({ length: max }, (_, i) => (\n <span key={i} className={`star ${i < Math.round(rating) ? \"filled\" : \"\"}`}>★</span>\n ))}\n </div>\n );\n}\n```\n\n```css\n.star { color: #ddd; font-size: 1.2em; }\n.star.filled { color: #ffc107; }\n```\n\n## react-slider / react-compound-slider → Native Range Input\n\n```tsx\nfunction RangeSlider({ min, max, value, onChange }: { min: number; max: number; value: number; onChange: (v: number) => void }) {\n return (\n <input\n type=\"range\"\n min={min}\n max={max}\n value={value}\n onInput={(e) => onChange(Number((e.target as HTMLInputElement).value))}\n className=\"range-slider\"\n />\n );\n}\n```\n\n## react-hot-toast → Simple Toast\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction Toast({ message, duration = 3000, onClose }: { message: string; duration?: number; onClose: () => void }) {\n useEffect(() => {\n const timer = setTimeout(onClose, duration);\n return () => clearTimeout(timer);\n }, [duration, onClose]);\n\n return <div className=\"toast\">{message}</div>;\n}\n```\n\n```css\n.toast {\n position: fixed; bottom: 20px; right: 20px;\n background: #333; color: white; padding: 12px 24px;\n border-radius: 8px; z-index: 9999;\n animation: slideIn 0.3s ease;\n}\n@keyframes slideIn { from { transform: translateY(20px); opacity: 0; } }\n```\n\n## react-fast-marquee → CSS Marquee\n\n```css\n.marquee-container { overflow: hidden; }\n.marquee-track {\n display: flex; width: max-content;\n animation: marquee 20s linear infinite;\n}\n@keyframes marquee {\n 0% { transform: translateX(0); }\n 100% { transform: translateX(-50%); }\n}\n```\nDuplicate the content so it loops seamlessly.\n\n## react-indiana-drag-scroll → CSS Overflow Scroll\n\n```css\n.drag-scroll {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n cursor: grab;\n}\n.drag-scroll::-webkit-scrollbar { display: none; }\n```\n\n## react-simple-typewriter → setInterval Typewriter\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction Typewriter({ words, speed = 100 }: { words: string[]; speed?: number }) {\n const [text, setText] = useState(\"\");\n const [wordIndex, setWordIndex] = useState(0);\n const [charIndex, setCharIndex] = useState(0);\n const [deleting, setDeleting] = useState(false);\n\n useEffect(() => {\n const word = words[wordIndex];\n const timer = setTimeout(() => {\n if (!deleting) {\n setText(word.slice(0, charIndex + 1));\n if (charIndex + 1 === word.length) {\n setTimeout(() => setDeleting(true), 1500);\n } else {\n setCharIndex(c => c + 1);\n }\n } else {\n setText(word.slice(0, charIndex));\n if (charIndex === 0) {\n setDeleting(false);\n setWordIndex((i) => (i + 1) % words.length);\n } else {\n setCharIndex(c => c - 1);\n }\n }\n }, deleting ? speed / 2 : speed);\n return () => clearTimeout(timer);\n }, [charIndex, deleting, wordIndex, words, speed]);\n\n return <span>{text}<span className=\"cursor\">|</span></span>;\n}\n```\n\n## react-timer-hook → useEffect Countdown\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction useCountdown(targetDate: Date) {\n const [timeLeft, setTimeLeft] = useState(getTimeLeft(targetDate));\n\n useEffect(() => {\n const interval = setInterval(() => setTimeLeft(getTimeLeft(targetDate)), 1000);\n return () => clearInterval(interval);\n }, [targetDate]);\n\n return timeLeft;\n}\n\nfunction getTimeLeft(target: Date) {\n const diff = Math.max(0, target.getTime() - Date.now());\n return {\n days: Math.floor(diff / 86400000),\n hours: Math.floor((diff % 86400000) / 3600000),\n minutes: Math.floor((diff % 3600000) / 60000),\n seconds: Math.floor((diff % 60000) / 1000),\n };\n}\n```\n\n## date-fns → Native Intl / Inline Utils\n\n```tsx\nfunction formatDate(date: Date | string, locale = \"tr-TR\"): string {\n return new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\" }).format(new Date(date));\n}\n\nfunction formatRelative(date: Date | string): string {\n const diff = Date.now() - new Date(date).getTime();\n const days = Math.floor(diff / 86400000);\n if (days === 0) return \"Today\";\n if (days === 1) return \"Yesterday\";\n if (days < 7) return `${days} days ago`;\n return formatDate(date);\n}\n```\n\n## slugify → Inline Slugify\n\n```tsx\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n```\n\n## classnames → Template Literals\n\nOld: `classnames('base', { active: isActive, hidden: !visible })`\nNew: `` `base ${isActive ? 'active' : ''} ${!visible ? 'hidden' : ''}`.trim() ``\n\nOr a simple helper:\n```tsx\nfunction cn(...classes: (string | false | undefined | null)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n```\n\n## tailwindcss → Plain CSS\n\nConvert Tailwind utility classes to equivalent CSS. Common patterns:\n\n| Tailwind | CSS |\n|----------|-----|\n| `flex` | `display: flex` |\n| `grid grid-cols-2` | `display: grid; grid-template-columns: repeat(2, 1fr)` |\n| `gap-4` | `gap: 1rem` |\n| `p-6` | `padding: 1.5rem` |\n| `mt-4` | `margin-top: 1rem` |\n| `text-center` | `text-align: center` |\n| `text-[20px]` | `font-size: 20px` |\n| `font-light` | `font-weight: 300` |\n| `rounded-lg` | `border-radius: 0.5rem` |\n| `hidden lg:block` | `display: none; @media (min-width: 1024px) { display: block; }` |\n| `hover:opacity-80` | `.class:hover { opacity: 0.8; }` |\n| `transition-all` | `transition: all 0.3s ease` |\n| `absolute inset-0` | `position: absolute; top: 0; right: 0; bottom: 0; left: 0` |\n| `overflow-hidden` | `overflow: hidden` |\n| `prose` | Custom rich text styles (font sizes, line heights, margins for h1-h6, p, ul, ol) |\n\nTailwind breakpoints: `sm` = 640px, `md` = 768px, `lg` = 1024px, `xl` = 1280px, `2xl` = 1536px.\n\n## @react-pdf/renderer\n\nPDF generation cannot be replicated in code components. If the old theme generates PDFs, this functionality must be dropped or handled externally.\n\n## @heroicons/react → Inline SVG\n\nReplace icon components with inline SVG. Copy the SVG paths from the Heroicons website and create simple components:\n```tsx\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" className={className} width=\"20\" height=\"20\">\n <path fillRule=\"evenodd\" d=\"M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z\" clipRule=\"evenodd\" />\n </svg>\n );\n}\n```",
|
|
51
|
+
"content": "The new code-component system does not support external libraries. Here's how to replace each common library found in old themes.\n\n## swiper \u2192 CSS Scroll-Snap Carousel\n\n**What it does:** Touch-enabled carousel/slider with autoplay, pagination, navigation.\n\n**Replacement pattern:**\n```tsx\nimport { useRef, useState, useEffect } from \"preact/hooks\";\n\nfunction Carousel({ children }: { children: any[] }) {\n const trackRef = useRef<HTMLDivElement>(null);\n const [current, setCurrent] = useState(0);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n const handleScroll = () => {\n const index = Math.round(track.scrollLeft / track.clientWidth);\n setCurrent(index);\n };\n track.addEventListener(\"scroll\", handleScroll, { passive: true });\n return () => track.removeEventListener(\"scroll\", handleScroll);\n }, []);\n\n const goTo = (index: number) => {\n trackRef.current?.scrollTo({ left: index * trackRef.current.clientWidth, behavior: \"smooth\" });\n };\n\n return (\n <div className=\"carousel\">\n <div ref={trackRef} className=\"carousel-track\">\n {children}\n </div>\n <div className=\"carousel-dots\">\n {children.map((_, i) => (\n <button\n key={i}\n className={`carousel-dot ${i === current ? \"active\" : \"\"}`}\n onClick={() => goTo(i)}\n />\n ))}\n </div>\n </div>\n );\n}\n```\n\n**CSS:**\n```css\n.carousel-track {\n display: flex;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n -webkit-overflow-scrolling: touch;\n}\n.carousel-track::-webkit-scrollbar { display: none; }\n.carousel-track > * {\n flex: 0 0 100%;\n scroll-snap-align: start;\n}\n.carousel-dot {\n width: 8px; height: 8px; border-radius: 50%;\n background: #ccc; border: none; cursor: pointer;\n}\n.carousel-dot.active { background: #333; }\n```\n\n**For autoplay**, add a `useEffect` with `setInterval` that calls `goTo((current + 1) % count)`.\n\n**For `slidesPerView: \"auto\"`**, remove `flex: 0 0 100%` and let children size themselves.\n\n## @headlessui/react \u2192 Custom Preact Components\n\n**Disclosure (accordion):**\n```tsx\nimport { useState } from \"preact/hooks\";\n\nfunction Disclosure({ title, children }: { title: string; children: any }) {\n const [open, setOpen] = useState(false);\n return (\n <div>\n <button onClick={() => setOpen(!open)} className=\"disclosure-btn\">\n {title}\n <span className={`arrow ${open ? \"open\" : \"\"}`}>\u25b8</span>\n </button>\n {open && <div className=\"disclosure-panel\">{children}</div>}\n </div>\n );\n}\n```\n\n**Dialog (modal):**\n```tsx\nimport { useEffect, useRef } from \"preact/hooks\";\n\nfunction Dialog({ open, onClose, children }: { open: boolean; onClose: () => void; children: any }) {\n const overlayRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (open) document.body.style.overflow = \"hidden\";\n else document.body.style.overflow = \"\";\n return () => { document.body.style.overflow = \"\"; };\n }, [open]);\n\n if (!open) return null;\n return (\n <div ref={overlayRef} className=\"dialog-overlay\" onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}>\n <div className=\"dialog-content\">{children}</div>\n </div>\n );\n}\n```\n\n## recharts \u2192 SVG or CSS Charts\n\nFor simple bar/line charts, use inline SVG. For complex charts, consider CSS-based bar charts:\n```tsx\nfunction BarChart({ data }: { data: { label: string; value: number }[] }) {\n const max = Math.max(...data.map(d => d.value));\n return (\n <div className=\"bar-chart\">\n {data.map((d, i) => (\n <div key={i} className=\"bar-item\">\n <div className=\"bar\" style={{ height: `${(d.value / max) * 100}%` }} />\n <span className=\"bar-label\">{d.label}</span>\n </div>\n ))}\n </div>\n );\n}\n```\n\n## react-player \u2192 Native Video/Iframe\n\n```tsx\nfunction VideoPlayer({ url, poster }: { url: string; poster?: string }) {\n if (url.includes(\"youtube.com\") || url.includes(\"youtu.be\")) {\n const videoId = url.match(/(?:v=|youtu\\.be\\/)([^&]+)/)?.[1];\n return <iframe src={`https://www.youtube.com/embed/${videoId}`} allowFullScreen className=\"video-iframe\" />;\n }\n if (url.includes(\"vimeo.com\")) {\n const videoId = url.match(/vimeo\\.com\\/(\\d+)/)?.[1];\n return <iframe src={`https://player.vimeo.com/video/${videoId}`} allowFullScreen className=\"video-iframe\" />;\n }\n return <video src={url} poster={poster} controls className=\"video-native\" />;\n}\n```\n\n## react-simple-star-rating \u2192 CSS Star Rating\n\n```tsx\nfunction StarRating({ rating, max = 5 }: { rating: number; max?: number }) {\n return (\n <div className=\"star-rating\">\n {Array.from({ length: max }, (_, i) => (\n <span key={i} className={`star ${i < Math.round(rating) ? \"filled\" : \"\"}`}>\u2605</span>\n ))}\n </div>\n );\n}\n```\n\n```css\n.star { color: #ddd; font-size: 1.2em; }\n.star.filled { color: #ffc107; }\n```\n\n## react-slider / react-compound-slider \u2192 Native Range Input\n\n```tsx\nfunction RangeSlider({ min, max, value, onChange }: { min: number; max: number; value: number; onChange: (v: number) => void }) {\n return (\n <input\n type=\"range\"\n min={min}\n max={max}\n value={value}\n onInput={(e) => onChange(Number((e.target as HTMLInputElement).value))}\n className=\"range-slider\"\n />\n );\n}\n```\n\n## react-hot-toast \u2192 Simple Toast\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction Toast({ message, duration = 3000, onClose }: { message: string; duration?: number; onClose: () => void }) {\n useEffect(() => {\n const timer = setTimeout(onClose, duration);\n return () => clearTimeout(timer);\n }, [duration, onClose]);\n\n return <div className=\"toast\">{message}</div>;\n}\n```\n\n```css\n.toast {\n position: fixed; bottom: 20px; right: 20px;\n background: #333; color: white; padding: 12px 24px;\n border-radius: 8px; z-index: 9999;\n animation: slideIn 0.3s ease;\n}\n@keyframes slideIn { from { transform: translateY(20px); opacity: 0; } }\n```\n\n## react-fast-marquee \u2192 CSS Marquee\n\n```css\n.marquee-container { overflow: hidden; }\n.marquee-track {\n display: flex; width: max-content;\n animation: marquee 20s linear infinite;\n}\n@keyframes marquee {\n 0% { transform: translateX(0); }\n 100% { transform: translateX(-50%); }\n}\n```\nDuplicate the content so it loops seamlessly.\n\n## react-indiana-drag-scroll \u2192 CSS Overflow Scroll\n\n```css\n.drag-scroll {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n cursor: grab;\n}\n.drag-scroll::-webkit-scrollbar { display: none; }\n```\n\n## react-simple-typewriter \u2192 setInterval Typewriter\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction Typewriter({ words, speed = 100 }: { words: string[]; speed?: number }) {\n const [text, setText] = useState(\"\");\n const [wordIndex, setWordIndex] = useState(0);\n const [charIndex, setCharIndex] = useState(0);\n const [deleting, setDeleting] = useState(false);\n\n useEffect(() => {\n const word = words[wordIndex];\n const timer = setTimeout(() => {\n if (!deleting) {\n setText(word.slice(0, charIndex + 1));\n if (charIndex + 1 === word.length) {\n setTimeout(() => setDeleting(true), 1500);\n } else {\n setCharIndex(c => c + 1);\n }\n } else {\n setText(word.slice(0, charIndex));\n if (charIndex === 0) {\n setDeleting(false);\n setWordIndex((i) => (i + 1) % words.length);\n } else {\n setCharIndex(c => c - 1);\n }\n }\n }, deleting ? speed / 2 : speed);\n return () => clearTimeout(timer);\n }, [charIndex, deleting, wordIndex, words, speed]);\n\n return <span>{text}<span className=\"cursor\">|</span></span>;\n}\n```\n\n## react-timer-hook \u2192 useEffect Countdown\n\n```tsx\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction useCountdown(targetDate: Date) {\n const [timeLeft, setTimeLeft] = useState(getTimeLeft(targetDate));\n\n useEffect(() => {\n const interval = setInterval(() => setTimeLeft(getTimeLeft(targetDate)), 1000);\n return () => clearInterval(interval);\n }, [targetDate]);\n\n return timeLeft;\n}\n\nfunction getTimeLeft(target: Date) {\n const diff = Math.max(0, target.getTime() - Date.now());\n return {\n days: Math.floor(diff / 86400000),\n hours: Math.floor((diff % 86400000) / 3600000),\n minutes: Math.floor((diff % 3600000) / 60000),\n seconds: Math.floor((diff % 60000) / 1000),\n };\n}\n```\n\n## date-fns \u2192 Native Intl / Inline Utils\n\n```tsx\nfunction formatDate(date: Date | string, locale = \"tr-TR\"): string {\n return new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\" }).format(new Date(date));\n}\n\nfunction formatRelative(date: Date | string): string {\n const diff = Date.now() - new Date(date).getTime();\n const days = Math.floor(diff / 86400000);\n if (days === 0) return \"Today\";\n if (days === 1) return \"Yesterday\";\n if (days < 7) return `${days} days ago`;\n return formatDate(date);\n}\n```\n\n## slugify \u2192 Inline Slugify\n\n```tsx\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n```\n\n## classnames \u2192 Template Literals\n\nOld: `classnames('base', { active: isActive, hidden: !visible })`\nNew: `` `base ${isActive ? 'active' : ''} ${!visible ? 'hidden' : ''}`.trim() ``\n\nOr a simple helper:\n```tsx\nfunction cn(...classes: (string | false | undefined | null)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n```\n\n## tailwindcss \u2192 Plain CSS\n\nConvert Tailwind utility classes to equivalent CSS. Common patterns:\n\n| Tailwind | CSS |\n|----------|-----|\n| `flex` | `display: flex` |\n| `grid grid-cols-2` | `display: grid; grid-template-columns: repeat(2, 1fr)` |\n| `gap-4` | `gap: 1rem` |\n| `p-6` | `padding: 1.5rem` |\n| `mt-4` | `margin-top: 1rem` |\n| `text-center` | `text-align: center` |\n| `text-[20px]` | `font-size: 20px` |\n| `font-light` | `font-weight: 300` |\n| `rounded-lg` | `border-radius: 0.5rem` |\n| `hidden lg:block` | `display: none; @media (min-width: 1024px) { display: block; }` |\n| `hover:opacity-80` | `.class:hover { opacity: 0.8; }` |\n| `transition-all` | `transition: all 0.3s ease` |\n| `absolute inset-0` | `position: absolute; top: 0; right: 0; bottom: 0; left: 0` |\n| `overflow-hidden` | `overflow: hidden` |\n| `prose` | Custom rich text styles (font sizes, line heights, margins for h1-h6, p, ul, ol) |\n\nTailwind breakpoints: `sm` = 640px, `md` = 768px, `lg` = 1024px, `xl` = 1280px, `2xl` = 1536px.\n\n## @react-pdf/renderer\n\nPDF generation cannot be replicated in code components. If the old theme generates PDFs, this functionality must be dropped or handled externally.\n\n## @heroicons/react \u2192 Inline SVG\n\nReplace icon components with inline SVG. Copy the SVG paths from the Heroicons website and create simple components:\n```tsx\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" className={className} width=\"20\" height=\"20\">\n <path fillRule=\"evenodd\" d=\"M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z\" clipRule=\"evenodd\" />\n </svg>\n );\n}\n```",
|
|
52
52
|
"tags": [
|
|
53
53
|
"migration",
|
|
54
54
|
"libraries",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"react-to-preact": {
|
|
71
71
|
"title": "React to Preact Conversion Patterns",
|
|
72
72
|
"description": "Code patterns for converting old React components to new Preact code components",
|
|
73
|
-
"content": "## CRITICAL: Two Completely Different Systems\n\n**`@ikas/storefront` and `@ikas/bp-storefront` are entirely separate packages.** Types with the same name are NOT the same types
|
|
73
|
+
"content": "## CRITICAL: Two Completely Different Systems\n\n**`@ikas/storefront` and `@ikas/bp-storefront` are entirely separate packages.** Types with the same name are NOT the same types \u2014 they come from different packages with different properties. Never copy old `@ikas/storefront` type usage patterns into new code. Always use the MCP tools (`get_type_definition`, `get_function_doc`, `get_model_guide`) to look up the correct API in the new system.\n\n## Import Changes\n\n### Remove React import\nOld: `import React from 'react';` or `import * as React from 'react';`\nNew: Remove entirely. Preact JSX is auto-imported.\n\n### Hook imports\nOld: `import { useState, useEffect, useRef, useMemo, useCallback } from 'react';`\nNew: `import { useState, useEffect, useRef, useMemo, useCallback } from 'preact/hooks';`\n\n### Storefront imports\nOld: `import { Image, ... } from '@ikas/storefront';`\nNew: `import { getDefaultSrc, ... } from '@ikas/bp-storefront';`\n\n**WARNING:** Do not assume that because `IkasImage` exists in both packages, it has the same shape. The old `IkasImage` from `@ikas/storefront` and the new `IkasImage` from `@ikas/bp-storefront` are different types. Always check the new type definition via `get_type_definition('IkasImage')` before using it.\n\nThe old `<Image>` component from `@ikas/storefront` does not exist in the new system. Use native `<img>` with `getDefaultSrc()`:\n```tsx\n// Old:\nimport { Image } from '@ikas/storefront';\n<Image image={myImage} layout=\"fill\" objectFit=\"cover\" sizes=\"300px\" />\n\n// New:\nimport { getDefaultSrc } from '@ikas/bp-storefront';\n<img src={getDefaultSrc(myImage)} alt=\"\" className=\"my-image\" />\n```\n\nFor responsive images, use `getSrc()` with width parameter or `createMediaSrcset()`.\n\n### Observer pattern\nOld: `import { observer } from 'mobx-react-lite'; export default observer(MyComponent);`\nNew: Remove for root components. Only use on sub-components:\n```tsx\n// Root component (in src/components/) \u2014 NO observer:\nexport default function MySection(props: Props) { ... }\n\n// Sub-component (in src/sub-components/) \u2014 observer OK:\nimport { observer } from '@ikas/component-utils';\nfunction CartCounter() { ... }\nexport default observer(CartCounter);\n```\n\n## Component Declaration\n\nOld:\n```tsx\nconst MyComponent = ({ title, image }: MyComponentProps) => { return (...); };\nexport default observer(MyComponent);\n```\n\nNew:\n```tsx\nimport { Props } from './types';\n\nexport function MyComponent({ title, image }: Props) { return (...); }\nexport default MyComponent;\n```\n\n**CRITICAL: Components need BOTH a named export AND a default export.** The CLI auto-generates a barrel file (`src/components/index.ts`) that uses named imports: `export { MyComponent } from './MyComponent/index'`. If you only do `export default function MyComponent`, the barrel import will fail with type errors. Always declare the function with `export function Name(...)` and then add `export default Name;` below it.\n\nKey changes:\n- Named function export AND default export (both required)\n- Props type imported from `./types` (auto-generated by CLI)\n- No `observer()` wrapper on root components\n\n## Type Changes\n\n| Old Type | New Type | Notes |\n|----------|----------|-------|\n| `React.CSSProperties` | `Record<string, string>` | Inline-style object |\n| `React.FC<Props>` | Remove, use named function | |\n| `React.MouseEvent` | `Event` (cast `e.target` as needed) | `JSX.TargetedMouseEvent<T>` is not reliably resolvable in our tsconfig \u2014 prefer plain `Event`. |\n| `React.ChangeEvent<HTMLInputElement>` | `Event` + cast | Preact uses `onInput` (not `onChange`) for text inputs. |\n| `React.FormEvent` | `Event` + cast | |\n| `React.ReactNode` | `ComponentChildren` from `preact` | |\n| `IkasSlider` | `number` | Was `{ value: number }`, now plain number |\n\n## CSS Patterns\n\n### Tailwind \u2192 Scoped CSS\nOld: `<div className=\"flex items-center gap-4 p-6 text-center\">`\nNew: `<div className=\"my-container\">` with CSS:\n```css\n.my-container {\n display: flex;\n align-items: center;\n gap: 1rem;\n padding: 1.5rem;\n text-align: center;\n}\n```\n\n### CSS Modules \u2192 Scoped CSS\nOld: `import styles from './styles.module.css'; <div className={styles.container}>`\nNew: `<div className=\"container\">` \u2014 CSS is auto-scoped via `.cc_{componentId}` prefix.\n\n### Inline styles with CSS custom properties\nOld: `style={{ '--mth': \\`\\${marginTop?.value}px\\` } as React.CSSProperties}`\nNew: `style={{ '--mth': \\`\\${marginTop}px\\` }}` \u2014 Note: `marginTop` is now a plain number, not `IkasSlider`.\n\n## IkasSlider \u2192 number\n\nThis is one of the most common code changes. Every `IkasSlider` value access needs updating:\n\n```tsx\n// Old (IkasSlider has .value property):\nconst width = logoWidth?.value || 120;\nconst gap = `${spacing?.value || 0}px`;\nstyle={{ '--w': `${item.width?.value}px` }}\n\n// New (plain number):\nconst width = logoWidth || 120;\nconst gap = `${spacing || 0}px`;\nstyle={{ '--w': `${item.width}px` }}\n```\n\n## Image Handling\n\n```tsx\n// Old:\nimport { Image } from '@ikas/storefront';\n<Image image={myImage} layout=\"fill\" objectFit=\"cover\" sizes=\"300px\" />\n<Image image={myImage} layout=\"responsive\" width={300} height={200} />\n\n// New:\nimport { getDefaultSrc, getSrc } from '@ikas/bp-storefront';\n\n// Simple image:\n{myImage && <img src={getDefaultSrc(myImage)} alt=\"\" />}\n\n// With specific size:\n{myImage && <img src={getSrc(myImage, { width: 300 })} alt=\"\" />}\n\n// Background image:\n<div style={{ backgroundImage: myImage ? `url(${getDefaultSrc(myImage)})` : 'none' }} />\n```\n\n## Video Handling\n\n```tsx\n// Old (IkasVideo was often used with react-player):\nimport ReactPlayer from 'react-player';\n<ReactPlayer url={video.url} />\n\n// New (native video or iframe):\n{video && (\n video.type === 'EMBED'\n ? <iframe src={video.url} allowFullScreen />\n : <video src={video.url} controls poster={video.thumbnailUrl} />\n)}\n```\n\n## Event Handling\n\n```tsx\n// Old:\nconst handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setValue(e.target.value);\n};\n\n// New \u2014 use plain `Event` and cast (works regardless of tsconfig JSX resolution):\nconst handleInput = (e: Event) => {\n setValue((e.target as HTMLInputElement).value);\n};\n\n// Preact uses `onInput` (not `onChange`) for text inputs:\n// <input onInput={handleInput} />\n```\n\n## dangerouslySetInnerHTML\n\nWorks the same in both:\n```tsx\n<div dangerouslySetInnerHTML={{ __html: richTextContent }} />\n```\n\n## useStore \u2192 Direct imports\n\nOld themes used `useStore()` from `@ikas/storefront` for accessing MobX stores:\n```tsx\n// Old:\nconst { cartStore, customerStore } = useStore();\n\n// New \u2014 import stores directly:\nimport { cartStore, customerStore } from '@ikas/bp-storefront';\n```\n\n## key prop\n\nSame in both systems. Always use `key` when mapping:\n```tsx\n{items.map((item, i) => <div key={item.id || i}>...</div>)}\n```",
|
|
74
74
|
"tags": [
|
|
75
75
|
"migration",
|
|
76
76
|
"react",
|
|
@@ -86,9 +86,9 @@
|
|
|
86
86
|
]
|
|
87
87
|
},
|
|
88
88
|
"storefront-import-mapping": {
|
|
89
|
-
"title": "Import Mapping: @ikas/storefront
|
|
89
|
+
"title": "Import Mapping: @ikas/storefront \u2192 @ikas/bp-storefront",
|
|
90
90
|
"description": "Maps old @ikas/storefront imports to new @ikas/bp-storefront equivalents",
|
|
91
|
-
"content": "## Old Package
|
|
91
|
+
"content": "## Old Package \u2192 New Package\n\nAll imports change from `@ikas/storefront` to `@ikas/bp-storefront`. **This is NOT a find-and-replace.** Even types with the same name (`IkasImage`, `IkasProduct`, etc.) come from different packages and may have different shapes. Always verify with `get_type_definition` or `get_model_guide` before assuming compatibility.\n\n## Type Rename Table\n\n| Old Import (`@ikas/storefront`) | New Import (`@ikas/bp-storefront`) | Notes |\n|-----------|-----------|-------|\n| `IkasImage`, `IkasVideo`, `IkasNavigationLink`, `IkasProduct`, `IkasProductList`, `IkasCategory`, `IkasCategoryList`, `IkasBrand`, `IkasBlog`, `IkasBlogList`, `IkasBlogCategory`, `IkasBlogCategoryList` | Same name | Different package \u2014 shape may differ; verify before assuming. |\n| `IkasSlider` | *(removed)* | Use plain `number`. |\n| `IkasAttributeDetail` | `IkasProductAttributeValue` | Renamed. |\n| `IkasAttributeList` | `IkasProductAttributeValue[]` | Renamed; now a plain array. |\n| `IkasComponentRenderer` | `IkasComponentRenderer` | Same name, different usage \u2014 see below. |\n\n## Function / Pattern Rename Table\n\n| Old | New | Notes |\n|-----|-----|-------|\n| `<Image>` component | `getDefaultSrc()` / `getSrc()` + native `<img>` | See `react-to-preact` for image patterns. |\n| `useStore()` hook | Direct imports (`cartStore`, `customerStore`, ...) | Stores are MobX observables; no wrapper hook. |\n| `<IkasComponentRenderer>{children}</...>` | `<IkasComponentRenderer id=\"...\" components={list as any[]} parentProps={props} />` | New props-based API. |\n\nFor all import/observer/hook/event-handling behavior changes, see `get_migration_guide(\"react-to-preact\")` \u2014 this topic is intentionally a quick rename reference.\n",
|
|
92
92
|
"tags": [
|
|
93
93
|
"migration",
|
|
94
94
|
"imports",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"theme-json-anatomy": {
|
|
104
104
|
"title": "Old theme.json Structure",
|
|
105
105
|
"description": "Explains the structure of old theme.json files for interpreting analyze_old_theme output",
|
|
106
|
-
"content": "## Top-Level Structure\n\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"Theme Name\",\n \"components\": [...],\n \"pages\": [...],\n \"customData\": [...],\n \"groups\": [...],\n \"settings\": { \"colors\": [...], \"fontFamily\": {...}, \"favicon\": {...}, \"stockPreference\": \"...\" },\n \"languages\": [...]\n}\n```\n\n## components[]\n\nEach component is a reusable UI element:\n```json\n{\n \"id\": \"uuid-or-name\",\n \"dir\": \"ComponentDirName\",\n \"displayName\": \"Human Name\",\n \"description\": \"...\",\n \"type\": \"COMPONENT\",\n \"props\": [...],\n \"defaultPropValues\": { \"propName\": null },\n \"defaultPropValuesTranslations\": { \"locale\": { \"propName\": \"...\" } },\n \"commonPropValues\": {},\n \"translations\": {}\n}\n```\n\n`dir` is the component's directory under `src/components/`. It maps to the React component file at `src/components/{dir}/index.tsx`.\n\n## components[].props[]\n\nEach prop definition:\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"propName\",\n \"displayName\": \"Prop Label\",\n \"description\": \"\",\n \"type\": \"TEXT|BOOLEAN|IMAGE|CUSTOM|SLIDER|...\",\n \"isRequired\": true,\n \"customDataId\": \"uuid-or-null\",\n \"sliderData\": { \"min\": 0, \"max\": 100, \"interval\": 1 },\n \"groupId\": \"uuid-or-empty\",\n \"translations\": {},\n \"excludedFields\": [],\n \"excludedFieldsCategory\": [],\n \"isFacetListNotSent\": false\n}\n```\n\nKey fields:\n- `type`
|
|
106
|
+
"content": "## Top-Level Structure\n\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"Theme Name\",\n \"components\": [...],\n \"pages\": [...],\n \"customData\": [...],\n \"groups\": [...],\n \"settings\": { \"colors\": [...], \"fontFamily\": {...}, \"favicon\": {...}, \"stockPreference\": \"...\" },\n \"languages\": [...]\n}\n```\n\n## components[]\n\nEach component is a reusable UI element:\n```json\n{\n \"id\": \"uuid-or-name\",\n \"dir\": \"ComponentDirName\",\n \"displayName\": \"Human Name\",\n \"description\": \"...\",\n \"type\": \"COMPONENT\",\n \"props\": [...],\n \"defaultPropValues\": { \"propName\": null },\n \"defaultPropValuesTranslations\": { \"locale\": { \"propName\": \"...\" } },\n \"commonPropValues\": {},\n \"translations\": {}\n}\n```\n\n`dir` is the component's directory under `src/components/`. It maps to the React component file at `src/components/{dir}/index.tsx`.\n\n## components[].props[]\n\nEach prop definition:\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"propName\",\n \"displayName\": \"Prop Label\",\n \"description\": \"\",\n \"type\": \"TEXT|BOOLEAN|IMAGE|CUSTOM|SLIDER|...\",\n \"isRequired\": true,\n \"customDataId\": \"uuid-or-null\",\n \"sliderData\": { \"min\": 0, \"max\": 100, \"interval\": 1 },\n \"groupId\": \"uuid-or-empty\",\n \"translations\": {},\n \"excludedFields\": [],\n \"excludedFieldsCategory\": [],\n \"isFacetListNotSent\": false\n}\n```\n\nKey fields:\n- `type` \u2014 The prop type (TEXT, BOOLEAN, IMAGE, CUSTOM, SLIDER, etc.)\n- `customDataId` \u2014 Only for `type: \"CUSTOM\"`. References a customData entry by ID.\n- `sliderData` \u2014 Only for `type: \"SLIDER\"`. Contains min, max, interval.\n- `groupId` \u2014 References a group in the `groups[]` array.\n\n## customData[]\n\nReusable data type definitions. Each entry defines a complex data structure:\n\n```json\n{\n \"id\": \"uuid\",\n \"name\": \"Type Name\",\n \"typescriptName\": \"TypeName\",\n \"type\": \"DYNAMIC_LIST|OBJECT|STATIC_LIST|ENUM\",\n \"isRequired\": true,\n \"isRoot\": true,\n \"level\": 0,\n \"nestedData\": [...],\n \"enumOptions\": [...],\n \"sliderData\": {},\n \"translations\": {}\n}\n```\n\n### DYNAMIC_LIST\nA variable-length list. `nestedData` contains one OBJECT entry describing the item shape:\n```json\n{ \"type\": \"DYNAMIC_LIST\", \"nestedData\": [\n { \"type\": \"OBJECT\", \"typescriptName\": \"Item\", \"nestedData\": [\n { \"key\": \"title\", \"type\": \"TEXT\" },\n { \"key\": \"image\", \"type\": \"IMAGE\" }\n ]}\n]}\n```\nThe OBJECT's `nestedData` fields have `key` properties that become TypeScript property names.\n\n### OBJECT\nA structured object with named fields. Each field in `nestedData` has a `key`:\n```json\n{ \"type\": \"OBJECT\", \"nestedData\": [\n { \"key\": \"fieldName\", \"type\": \"TEXT\", \"name\": \"Field Label\" }\n]}\n```\n\n### Nesting\nCustomData can nest arbitrarily. A DYNAMIC_LIST can contain an OBJECT that has fields which are themselves DYNAMIC_LIST or reference other customData via `customDataId`.\n\n## groups[]\n\nProp grouping definitions:\n```json\n{ \"id\": \"uuid\", \"name\": \"Group Name\", \"translations\": {} }\n```\nComponents reference groups via `groupId` on their props.\n\n## settings\n\n```json\n{\n \"colors\": [\n { \"id\": \"uuid\", \"displayName\": \"Color Name\", \"color\": \"#hexvalue\", \"key\": \"--css-var-name\" }\n ],\n \"fontFamily\": { \"name\": \"FontName\", \"variants\": [\"300\", \"regular\", \"500\", \"600\", \"700\"] },\n \"favicon\": { \"id\": \"uuid-or-null\" },\n \"stockPreference\": \"SHOW_ALL|HIDE_OUT_OF_STOCK\"\n}\n```\n\nColors define CSS custom properties (e.g., `--primary`, `--button-bg-1`). These should be converted to CSS variables in `global.css`.\n\n## pages[]\n\nPage definitions with component instances and their prop values. Not needed for component migration but useful for understanding which components are used where.",
|
|
107
107
|
"tags": [
|
|
108
108
|
"migration",
|
|
109
109
|
"theme-json",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"component-decomposition-strategy": {
|
|
120
120
|
"title": "Component Decomposition Strategy",
|
|
121
121
|
"description": "How to break old monolithic components into section + child components in the new system",
|
|
122
|
-
"content": "## Overview\n\nOld themes typically have flat component lists where each component handles everything internally. The new system uses a section
|
|
122
|
+
"content": "## Overview\n\nOld themes typically have flat component lists where each component handles everything internally. The new system uses a section \u2192 child component hierarchy. Here's how to decompose.\n\n## Rule 1: Every Old Component Becomes a Section\n\nIn the new system, sections are page-level containers. Each old component maps to a `type: \"section\"` in ikas.config.json.\n\nException: If an old component was only used as an internal sub-component (e.g., a reusable ProductCard), it becomes a `type: \"component\"` (child) instead.\n\n## Rule 2: CUSTOM Props \u2192 Pick the Right Shape\n\nFor each CUSTOM prop, do NOT default to `COMPONENT_LIST`. See `get_migration_guide(\"component-composition-decision-guide\")` for the full decision tree. Short version:\n- DYNAMIC_LIST of structured records with \u22653 fields \u2192 `COMPONENT_LIST` + new child component (use `filteredComponentIds`).\n- DYNAMIC_LIST of items with 1\u20132 fields \u2192 repeated scalar props or a domain LIST type (`LIST_OF_LINK`, `IMAGE_LIST`, \u2026).\n- OBJECT with \u22643 simple fields (TEXT/BOOLEAN/COLOR/NUMBER) \u2192 flatten into direct props on the parent.\n\nExample (flatten):\n- Old: `settings: { backgroundColor: string, showTitle: boolean }` via CUSTOM OBJECT\n- New: `backgroundColor` (COLOR) + `showTitle` (BOOLEAN) directly on section\n\n## Rule 4: Header and Footer\n\nIdentify header/footer components (usually named \"Navbar\", \"Header\", \"Footer\"):\n- Mark the section with `isHeader: true` or `isFooter: true`\n- These sections appear on every page automatically\n\n## Rule 5: Prop Groups\n\nOld `groups[]` array maps to `propGroups` on each component:\n```json\n// Old (global groups array):\n\"groups\": [{ \"id\": \"group-uuid\", \"name\": \"Slider\" }]\n// Component prop: { \"name\": \"delay\", \"groupId\": \"group-uuid\" }\n\n// New (per-component propGroups):\n\"propGroups\": [{ \"id\": \"slider\", \"name\": \"Slider\" }]\n// Component prop: { \"name\": \"delay\", \"groupId\": \"slider\" }\n```\n\n## Rule 6: Shared Sub-Components\n\nIf multiple sections share similar UI elements (buttons, modals, cards), create them in `src/sub-components/` instead of registering them in ikas.config.json.\n\n## Example Decomposition\n\n**Old: ProductGrid component** with props:\n- `products` (CUSTOM \u2192 DYNAMIC_LIST of `{ tab: string, products: IkasProductList }`)\n- `showedTags` (CUSTOM \u2192 DYNAMIC_LIST of `{ tag: string, color: string }`)\n- `columns` (SLIDER)\n- `gap` (SLIDER)\n\n**New structure:**\n1. **ProductGridSection** (section)\n - `tabs` \u2192 COMPONENT_LIST (filteredComponentIds: [\"my-project-product-tab\"])\n - `tags` \u2192 COMPONENT_LIST (filteredComponentIds: [\"my-project-product-tag\"])\n - `columns` \u2192 NUMBER\n - `gap` \u2192 NUMBER\n\n2. **ProductTab** (component)\n - `tabName` \u2192 TEXT\n - `products` \u2192 PRODUCT_LIST\n\n3. **ProductTag** (component)\n - `tag` \u2192 TEXT\n - `color` \u2192 COLOR\n - `backgroundColor` \u2192 COLOR\n\n## Estimating Child Components\n\nCount the number of CUSTOM props across all components that reference DYNAMIC_LIST customData. Each one typically needs 1 child component. Some may share child components if the structure is identical.",
|
|
123
123
|
"tags": [
|
|
124
124
|
"migration",
|
|
125
125
|
"decomposition",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"complete-project-generation": {
|
|
135
135
|
"title": "Complete Project Generation Guide",
|
|
136
136
|
"description": "How to generate a full code-component project from an old theme in one pass",
|
|
137
|
-
"content": "## Project Structure\n\nThe generated project should follow this structure:\n```\nmy-theme/\n
|
|
137
|
+
"content": "## Project Structure\n\nThe generated project should follow this structure:\n```\nmy-theme/\n\u251c\u2500\u2500 src/\n\u2502 \u251c\u2500\u2500 global.css\n\u2502 \u2514\u2500\u2500 components/\n\u2502 \u251c\u2500\u2500 HeaderSection/\n\u2502 \u2502 \u251c\u2500\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500\u2500 types.ts # Auto-generated (don't write manually)\n\u2502 \u2502 \u2514\u2500\u2500 styles.css\n\u2502 \u251c\u2500\u2500 HeroBanner/\n\u2502 \u2502 \u251c\u2500\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500\u2500 types.ts\n\u2502 \u2502 \u2514\u2500\u2500 styles.css\n\u2502 \u251c\u2500\u2500 SlideItem/ # Child component for hero slides\n\u2502 \u2502 \u251c\u2500\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500\u2500 types.ts\n\u2502 \u2502 \u2514\u2500\u2500 styles.css\n\u2502 \u251c\u2500\u2500 FooterSection/\n\u2502 \u2502 \u251c\u2500\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500\u2500 types.ts\n\u2502 \u2502 \u2514\u2500\u2500 styles.css\n\u2502 \u2514\u2500\u2500 index.ts # Barrel export\n\u251c\u2500\u2500 ikas.config.json\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 tsconfig.json\n\u2514\u2500\u2500 vite.config.ts\n```\n\n## Generation Order\n\n### 1. Generate ikas.config.json\n\nThis is the most critical file. It defines all components and their props.\n\n```json\n{\n \"name\": \"my-theme\",\n \"version\": \"1.0.0\",\n \"globalStyles\": \"./src/global.css\",\n \"components\": [\n {\n \"id\": \"my-theme-header\",\n \"name\": \"HeaderSection\",\n \"type\": \"section\",\n \"isHeader\": true,\n \"entry\": \"./src/components/HeaderSection/index.tsx\",\n \"styles\": \"./src/components/HeaderSection/styles.css\",\n \"props\": [...],\n \"propGroups\": [...]\n }\n ],\n \"customTypes\": [...]\n}\n```\n\nKey rules:\n- Component `id` format: `{projectName}-{component-kebab-case}`\n- Sections use `type: \"section\"`, children use `type: \"component\"` (or omit for default)\n- COMPONENT_LIST props should have `filteredComponentIds` pointing to specific child components\n- Create `customTypes` entries for all ENUM types\n\n### 2. Generate global.css\n\nConvert theme settings to CSS:\n```css\n/* Colors from settings.colors */\n:root {\n --ann-color: #ac8748;\n --fsp: #a9a9a9;\n --primary: #fed9d9;\n --button-bg-1: #000000;\n}\n\n/* Font from settings.fontFamily */\n@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');\n\nbody {\n font-family: 'Quicksand', sans-serif;\n}\n\n/* Common utility styles migrated from old global.css */\n.wrapper {\n max-width: 1700px;\n margin: 0 auto;\n padding: 0 20px;\n}\n\n@media (min-width: 1024px) {\n .wrapper {\n padding: 0 60px;\n }\n}\n```\n\n### 3. Generate Component Source Files\n\nFor each component, generate `index.tsx` and `styles.css`.\n\n**Do NOT generate `types.ts`** \u2014 the CLI auto-generates it from ikas.config.json.\n\nEach component should:\n- Import `Props` from `./types`\n- Use named default function export\n- Import from `@ikas/bp-storefront` (not `@ikas/storefront`)\n- Use `<IkasComponentRenderer>` for COMPONENT_LIST props\n- Use `getDefaultSrc()` for images\n- Use scoped CSS classes (not Tailwind)\n\n### 4. Barrel Export\n\n`src/components/index.ts`:\n```typescript\nexport { default as HeaderSection } from './HeaderSection';\nexport { default as HeroBanner } from './HeroBanner';\nexport { default as SlideItem } from './SlideItem';\n// ... etc\n```\n\n## Naming Conventions\n\n| Old | New |\n|----|-----|\n| Component name: `Navbar` | Section: `HeaderSection`, ID: `my-theme-header-section` |\n| Custom type: `Maaza` | Child component: `StoreCard`, ID: `my-theme-store-card` |\n| Custom type: `GrselveLink` | Child component: `BannerSlide`, ID: `my-theme-banner-slide` |\n\nUse descriptive English names even if the old theme used Turkish names.\n\n## Common Pitfalls\n\n1. **Don't forget `filteredComponentIds`** \u2014 Without it, any component can be placed in a COMPONENT_LIST slot\n2. **Don't manually write types.ts** \u2014 Always let the CLI generate it\n3. **Don't use `observer()` on root components** \u2014 Only on sub-components\n4. **Convert all `IkasSlider` usage** \u2014 `.value` access must become direct number access\n5. **Replace ALL library imports** \u2014 No swiper, headlessui, recharts, etc.\n6. **Convert Tailwind to plain CSS** \u2014 No utility classes in the new system",
|
|
138
138
|
"tags": [
|
|
139
139
|
"migration",
|
|
140
140
|
"project",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"settings-conversion": {
|
|
149
149
|
"title": "Theme Settings Conversion",
|
|
150
150
|
"description": "How to convert old theme settings (colors, fonts, favicon) to the new system",
|
|
151
|
-
"content": "## Colors\n\nOld theme settings define CSS custom properties:\n```json\n\"settings\": {\n \"colors\": [\n { \"displayName\": \"Primary\", \"color\": \"#fed9d9\", \"key\": \"--primary\" },\n { \"displayName\": \"Button BG\", \"color\": \"#000000\", \"key\": \"--button-bg-1\" }\n ]\n}\n```\n\nConvert to CSS variables in `global.css`:\n```css\n:root {\n --primary: #fed9d9;\n --button-bg-1: #000000;\n}\n```\n\nThese variables can then be used in component CSS files:\n```css\n.button { background-color: var(--button-bg-1); }\n```\n\nAlternatively, expose colors as COLOR props on individual components so store owners can customize per-component.\n\n## Fonts\n\nOld:\n```json\n\"fontFamily\": { \"name\": \"Quicksand\", \"variants\": [\"300\", \"regular\", \"500\", \"600\", \"700\"] }\n```\n\nNew `global.css`:\n```css\n@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');\n\nbody {\n font-family: 'Quicksand', sans-serif;\n}\n```\n\n## Favicon\n\nFavicon handling is outside the code-component scope. It's managed at the store level.\n\n## Stock Preference\n\nThe `stockPreference` setting (SHOW_ALL, HIDE_OUT_OF_STOCK) is handled by the storefront API, not component code.\n\n## See Also\n- `get_framework_guide(\"global-css\")`
|
|
151
|
+
"content": "## Colors\n\nOld theme settings define CSS custom properties:\n```json\n\"settings\": {\n \"colors\": [\n { \"displayName\": \"Primary\", \"color\": \"#fed9d9\", \"key\": \"--primary\" },\n { \"displayName\": \"Button BG\", \"color\": \"#000000\", \"key\": \"--button-bg-1\" }\n ]\n}\n```\n\nConvert to CSS variables in `global.css`:\n```css\n:root {\n --primary: #fed9d9;\n --button-bg-1: #000000;\n}\n```\n\nThese variables can then be used in component CSS files:\n```css\n.button { background-color: var(--button-bg-1); }\n```\n\nAlternatively, expose colors as COLOR props on individual components so store owners can customize per-component.\n\n## Fonts\n\nOld:\n```json\n\"fontFamily\": { \"name\": \"Quicksand\", \"variants\": [\"300\", \"regular\", \"500\", \"600\", \"700\"] }\n```\n\nNew `global.css`:\n```css\n@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');\n\nbody {\n font-family: 'Quicksand', sans-serif;\n}\n```\n\n## Favicon\n\nFavicon handling is outside the code-component scope. It's managed at the store level.\n\n## Stock Preference\n\nThe `stockPreference` setting (SHOW_ALL, HIDE_OUT_OF_STOCK) is handled by the storefront API, not component code.\n\n## See Also\n- `get_framework_guide(\"global-css\")` \u2014 full details on global.css and CSS variables\n- `get_framework_guide(\"css-scoping\")` \u2014 how component CSS is scoped vs global",
|
|
152
152
|
"tags": [
|
|
153
153
|
"migration",
|
|
154
154
|
"settings",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"finding-new-system-equivalents": {
|
|
162
162
|
"title": "Finding New-System Equivalents for Old APIs",
|
|
163
163
|
"description": "How to discover the new-system replacement for any old imperative API, store method, or feature. Use this instead of guessing or assuming old APIs still exist.",
|
|
164
|
-
"content": "The new system (`@ikas/bp-storefront`) is a **completely separate implementation** from the old system (`@ikas/storefront`). Most old imperative APIs have been replaced with form-model patterns, direct store imports, or dedicated functions. **Never assume an old API name works in the new system.** Look it up first.\n\n## The Discovery Protocol\n\nWhen you encounter any old-system API (e.g., `customerStore.createEmailSubscription`, `useStore()`, `<Image>`, `toast.success`, `Router.push`), follow this protocol:\n\n### Step 1: Extract the intent\n\nStrip the old API down to what it *does*, not how it's called. Examples:\n- `customerStore.createEmailSubscription(email)`
|
|
164
|
+
"content": "The new system (`@ikas/bp-storefront`) is a **completely separate implementation** from the old system (`@ikas/storefront`). Most old imperative APIs have been replaced with form-model patterns, direct store imports, or dedicated functions. **Never assume an old API name works in the new system.** Look it up first.\n\n## The Discovery Protocol\n\nWhen you encounter any old-system API (e.g., `customerStore.createEmailSubscription`, `useStore()`, `<Image>`, `toast.success`, `Router.push`), follow this protocol:\n\n### Step 1: Extract the intent\n\nStrip the old API down to what it *does*, not how it's called. Examples:\n- `customerStore.createEmailSubscription(email)` \u2192 intent: \"subscribe email to newsletter\"\n- `await customerStore.login({ email, password })` \u2192 intent: \"customer login\"\n- `searchStore.search(query)` \u2192 intent: \"product search\"\n- `cartStore.addProduct(variantId, qty)` \u2192 intent: \"add item to cart\"\n- `Router.push('/category/foo')` \u2192 intent: \"navigate to a page\"\n\n### Step 2: Search the new system\n\nUse one or more of these tools, in order of specificity:\n\n1. **`search_docs(\"<intent keywords>\")`** \u2014 searches across functions, framework topics, types, and migration guides. Always try this first.\n ```\n search_docs(\"newsletter email subscription\")\n search_docs(\"customer login\")\n search_docs(\"add to cart\")\n search_docs(\"navigate page\")\n ```\n\n2. **`list_functions(\"<category>\")`** \u2014 lists all functions in a category. Useful when you know the domain:\n ```\n list_functions(\"Form\") # newsletter, login, register, address forms\n list_functions(\"Cart\") # add/remove/update cart items\n list_functions(\"Customer\") # login, logout, register, profile\n list_functions(\"Navigation\") # Router.navigate and related\n list_functions(\"Validation\") # form validation helpers\n ```\n\n3. **`search_types(\"<keyword>\")`** \u2014 when you need a data shape:\n ```\n search_types(\"newsletter\")\n search_types(\"login form\")\n ```\n\n4. **`get_model_guide(\"<TypeName>\")`** \u2014 one-stop shop for a type: definition + all utility functions + examples + related types. Use once you've identified the relevant model.\n\n5. **`get_function_doc(\"<functionName>\")`** \u2014 exact signature, parameters, return type, example for a specific function.\n\n6. **`get_code_example(\"<task>\")`** \u2014 working code patterns for common tasks. Examples: `\"add to cart\"`, `\"variant selection\"`, `\"navigation\"`, `\"image-handling\"`, or specific sections like `\"login-section\"`, `\"cart-section\"`.\n\n7. **`get_framework_guide(\"form-handling\")`** / **`get_framework_guide(\"async-data-patterns\")`** \u2014 broader patterns that apply across many features.\n\n### Step 3: Recognize the common new-system patterns\n\nMany old imperative calls map to one of these new-system shapes:\n\n- **Forms (login, register, newsletter, contact, address, review submission):** look for a `get<Feature>Form()` + `submit<Feature>Form()` pair, usually with `set<Feature>Form<Field>()` setters. This is the universal form-model pattern.\n- **Cart / favorites / customer actions:** top-level functions exported directly from `@ikas/bp-storefront` (e.g. `addItemToCart`, `addToFavorites`) that mutate store state.\n- **Navigation:** `Router.navigate(href)` or `Router.navigateToPage(pageType, params)`.\n- **Page data (current product, category, blog):** imported stores read as observables (e.g. `productStore.product`, `categoryStore.category`).\n\n### Step 4: If you can't find it\n\nIf `search_docs` and `list_functions` return nothing relevant, the feature may:\n- Not exist in the new system (drop it, or use a TEXT prop for static content)\n- Be handled outside components (analytics, PDF export, etc.)\n- Require a creative workaround (e.g. a `date-fns` helper \u2192 inline `Intl.DateTimeFormat`)\n\nDocument what you couldn't find in the **Session Log** of MIGRATION.md so future sessions don't repeat the search.\n\n## Example: Migrating an Unfamiliar Old API\n\nSuppose the old Footer code does:\n```tsx\nimport { customerStore } from \"@ikas/storefront\";\nawait customerStore.createEmailSubscription(email);\ntoast.success(\"Subscribed!\");\n```\n\nDiscovery steps:\n1. `search_docs(\"email subscription newsletter\")` \u2192 finds `getNewsletterSubscriptionForm`, `submitNewsletterSubscriptionForm`, etc.\n2. `get_function_doc(\"submitNewsletterSubscriptionForm\")` \u2192 exact signature + example\n3. `get_framework_guide(\"form-handling\")` \u2192 the form-model pattern to apply\n4. For the `toast.success` call: this is a library replacement, not an ikas API. See `get_migration_guide(\"library-replacements\")` \u2014 it maps to inline status text with TEXT props.\n\n## Rule of Thumb\n\n**If the old code imports from `@ikas/storefront`, search before you write.** The function name is almost never the same in `@ikas/bp-storefront`, and many old methods have been replaced by entirely different patterns.\n\n## See Also\n- `get_migration_guide(\"storefront-import-mapping\")` \u2014 high-level type/function rename table\n- `get_migration_guide(\"react-to-preact\")` \u2014 imports, hooks, observer rules\n- `get_framework_guide(\"imports\")` \u2014 where each new-system function lives\n- `get_framework_guide(\"form-handling\")` \u2014 the form-model pattern all forms use",
|
|
165
165
|
"tags": [
|
|
166
166
|
"migration",
|
|
167
167
|
"discovery",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"iterative-workflow": {
|
|
184
184
|
"title": "Iterative Migration Workflow (Resumable, Section-by-Section)",
|
|
185
185
|
"description": "The recommended four-phase workflow for migrating large themes across multiple sessions. Use this for any theme with more than ~5 sections.",
|
|
186
|
-
"content": "## Why Iterative?\n\nA full theme migration (30-50 sections, dozens of customData types, hundreds of props) is too large to complete in a single LLM session. Without a persistent plan, each session re-derives the same decisions, wastes context, and produces inconsistent output.\n\n**The solution:** A `MIGRATION.md` file at the new project root that the MCP **writes once as an initial scaffold**. From that point on, the LLM owns the file
|
|
186
|
+
"content": "## Why Iterative?\n\nA full theme migration (30-50 sections, dozens of customData types, hundreds of props) is too large to complete in a single LLM session. Without a persistent plan, each session re-derives the same decisions, wastes context, and produces inconsistent output.\n\n**The solution:** A `MIGRATION.md` file at the new project root that the MCP **writes once as an initial scaffold**. From that point on, the LLM owns the file \u2014 ticks checkboxes, logs custom-data decisions, and corrects anything the MCP couldn't know.\n\n## MCP vs LLM Responsibilities\n\nThe MCP cannot do everything for you:\n\n- **MCP writes:** The initial MIGRATION.md (once, via `plan_migration` with `project_root`). After that the MCP never modifies the file.\n- **MCP provides:** Reference content via read-only tools (`get_section_migration_plan`, `get_migration_guide`, `get_section_template`, etc.).\n- **LLM owns:** All ongoing updates to MIGRATION.md \u2014 checkboxes, decisions log, source-code-analysis findings, notes.\n- **LLM must do:** Source code analysis. theme.json is incomplete; atomic components (Button, Input, Card, icons) often live only in `src/` and the MCP cannot see them. You must scan the old source.\n- **LLM decides:** Every customData enum-vs-component choice (see `get_migration_guide(\"custom-data-conversion\")`).\n\n## The Four Phases\n\n### Phase A: Plan + Source Analysis (run once)\n\n1. Call `plan_migration({theme_json_path, project_root, project_name, old_source_dir})`. Pass `theme_json_path` (absolute) \u2014 real-world theme.json files are too large to pass as a raw string. Pass `project_root` so the MCP writes MIGRATION.md directly.\n2. **Read the generated MIGRATION.md start-to-finish.** Pay attention to the \"READ THIS FIRST\" preamble.\n3. **Do source code analysis.** Scan the old project's `src/` for atomic components NOT referenced from theme.json \u2014 Button, Input, Card, icon wrappers, layout helpers, formatters. theme.json only lists theme-level components (sections); atomic primitives are invisible to the MCP. Add what you find under `## Source Code Analysis` in MIGRATION.md. Promote anything that's used by multiple sections to `### Shared Sub-Components` in the Foundation block.\n\n### Phase B: Foundation (run once)\n\nExecute the Foundation checklist in MIGRATION.md, in order:\n1. Create `src/global.css` with extracted CSS variables + font imports. Tick each `[ ]` to `[x]` as you complete it.\n2. Create each shared sub-component in `src/sub-components/<Name>/` (index.tsx + styles.css \u2014 NOT registered in ikas.config.json).\n3. **Note:** Custom enums are NOT pre-created. They are decided per-section in Phase C.\n\n### Phase C: Section Migration (loop)\n\nFor each section in the queue (start Simple, then Medium, then Complex):\n\n1. Call `get_section_migration_plan({theme_json_path, section_name, project_name})`. Returns: concrete CLI commands, prop-by-prop conversion table, and a **\"Custom Data Decisions to Make\"** callout listing every prop that references a customData type.\n2. Read the listed old source files.\n3. **Make customData decisions** for any callouts. For each: pick enum or component, run the corresponding CLI command, and **log the decision in MIGRATION.md under `## Custom Data Decisions`** with reasoning.\n4. Run the CLI commands (creates parent + children with auto-generated types.ts).\n5. Write `index.tsx` and `styles.css`.\n6. Mark the section `[x]` in MIGRATION.md (use your file-editing tool \u2014 there is no MCP tool for this).\n\n### Phase D: Resume (any session)\n\nThere is **no `resume_migration` tool**. The MCP is fully stateless after the initial `plan_migration` write.\n\n1. Read `<project_root>/MIGRATION.md` in full. The \"READ THIS FIRST\" preamble explains your responsibilities.\n2. Find the first unchecked `[ ]` in the Sections queue. That's where you resume.\n3. Check whether `## Source Code Analysis` is populated. If empty, the previous session didn't complete Phase A \u2014 finish it first.\n4. Check `## Custom Data Decisions` to see which customData types have already been resolved. Do NOT re-decide them.\n\n## Critical Rules\n\n- **MIGRATION.md is authoritative.** Component IDs, CSS variable names, and sub-component names listed there are final. Do not change them mid-migration.\n- **Update checkboxes immediately** as you finish work. This is how the next session knows where to resume.\n- **Log every customData decision.** Use the format documented in the file's commented example. Skipping this causes future sessions to re-decide and produce inconsistent component vs enum choices.\n- **Source code analysis is your job.** The MCP cannot find Button/Input/Card primitives \u2014 you must scan src/.\n- **Shared sub-components come BEFORE the sections that use them.** If a section depends on a sub-component that isn't yet `[x]`, build the sub-component first.\n- **For small themes (< 5 sections)** the iterative workflow is overkill \u2014 use direct implementation with `analyze_old_theme`.\n\n## See Also\n- `get_migration_guide(\"migration-overview\")` \u2014 big-picture system differences and responsibility split\n- `get_migration_guide(\"custom-data-conversion\")` \u2014 enum-vs-component decision framework with worked examples\n- `get_migration_guide(\"component-renderer-limitations\")` \u2014 critical constraints when using COMPONENT_LIST\n- `get_migration_guide(\"prop-runtime-shapes\")` \u2014 what each prop type actually receives at runtime\n",
|
|
187
187
|
"tags": [
|
|
188
188
|
"migration",
|
|
189
189
|
"workflow",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"component-renderer-limitations": {
|
|
199
199
|
"title": "IkasComponentRenderer: Critical Limitations",
|
|
200
200
|
"description": "Essential constraints to understand BEFORE designing sections with COMPONENT_LIST props. These limitations fundamentally shape migration decisions.",
|
|
201
|
-
"content": "`IkasComponentRenderer` is the mechanism that renders child components placed in `COMPONENT` and `COMPONENT_LIST` slots. It has important architectural constraints that affect how you migrate old CUSTOM/DYNAMIC_LIST props.\n\n## Constraint 1: Parent Cannot Read Child Prop Values\n\n**This is the single most important constraint.** When a store owner places child components in a `COMPONENT_LIST` slot, the parent component receives them as opaque `any[]` values. The parent cannot inspect their individual prop values.\n\n```tsx\nexport default function MySection({ items, ...props }: Props) {\n //
|
|
201
|
+
"content": "`IkasComponentRenderer` is the mechanism that renders child components placed in `COMPONENT` and `COMPONENT_LIST` slots. It has important architectural constraints that affect how you migrate old CUSTOM/DYNAMIC_LIST props.\n\n## Constraint 1: Parent Cannot Read Child Prop Values\n\n**This is the single most important constraint.** When a store owner places child components in a `COMPONENT_LIST` slot, the parent component receives them as opaque `any[]` values. The parent cannot inspect their individual prop values.\n\n```tsx\nexport default function MySection({ items, ...props }: Props) {\n // \u274c CANNOT do this \u2014 items[0].title is NOT accessible:\n // const firstTitle = items[0].title;\n\n // \u2705 CAN only do this \u2014 hand the list to IkasComponentRenderer:\n return (\n <section>\n <IkasComponentRenderer id=\"items\" components={items as any[]} parentProps={props} />\n </section>\n );\n}\n```\n\n### Migration Implication\n\nWhen an old component had logic that depended on child data (e.g., \"if any menu item has an icon, render the icon column\"), that logic must move **into** the child component \u2014 not stay in the parent.\n\n## Constraint 2: Child Components Access Parent Data Via `parentProps`\n\nChildren don't automatically see the parent's props. The parent must pass `parentProps={props}` to `IkasComponentRenderer`, and children access parent data via `privateVarMap` mapping or by receiving specific props declared in the parent's COMPONENT_LIST config.\n\n**For detailed patterns, use `get_framework_guide(\"private-var-map\")` and `get_framework_guide(\"component-renderer-patterns\")`.**\n\n## Constraint 3: `id` Must Be Unique Per Renderer Instance\n\nEach `<IkasComponentRenderer>` in your JSX must have a unique `id`, even when rendering the same `components` array. This matters for desktop/mobile dual renders:\n\n```tsx\n// Desktop menu\n<IkasComponentRenderer id=\"menu-desktop\" components={menuLinks as any[]} parentProps={props} />\n\n// Same list, mobile menu \u2014 different id\n<IkasComponentRenderer id=\"menu-mobile\" components={menuLinks as any[]} parentProps={props} />\n```\n\nReusing the same `id` in two renderer instances causes undefined behavior.\n\n## Decision Tree: What Replaces Old CUSTOM/DYNAMIC_LIST\n\nGiven the constraints above, here's how to decide:\n\n| Old data shape | New approach |\n|----------------|--------------|\n| List of simple links | `LIST_OF_LINK` prop \u2014 parent has full data access |\n| List of links with icon/color/label only | Depends: if parent needs to read them \u2192 use multiple flat props; if purely rendered \u2192 `COMPONENT_LIST` with a LinkItem child |\n| List of rich items (image + text + button + settings) | `COMPONENT_LIST` + child component (standard pattern) |\n| List where parent logic depends on item contents | **Rethink.** Either move the logic into the child, or flatten to multiple parent props |\n| Fixed-count structured data (e.g., 3-feature grid with title + icon each) | Either flatten into parent props (feature1Title, feature1Icon, ...) OR `COMPONENT_LIST` \u2014 choose based on whether order/count should be editor-controlled |\n| Deeply nested data (menu with sub-menu with images) | Nested `COMPONENT_LIST` across multiple child component levels |\n\n## Constraint 4: No Indexed Access to Children\n\nThe parent receives `COMPONENT_LIST` as an opaque array. You **cannot** access specific children by index for positional rendering:\n\n```tsx\n// \u274c CANNOT do this \u2014 old pattern that accessed children by position:\n// const firstChild = components[0];\n// const restChildren = components.slice(1);\n\n// \u2705 CAN only render the whole list at once:\n<IkasComponentRenderer id=\"items\" components={items as any[]} parentProps={props} />\n```\n\n### Migration Implication\n\nOld components sometimes used indexed access for layout purposes (e.g., a bento grid where `components[0]` spans 2 rows and `components[1:]` fill the rest). In the new system, use **CSS** to achieve positional layouts:\n\n```css\n/* CSS :nth-child selectors for positional layout */\n.bento-grid > :first-child { grid-row: span 2; }\n```\n\nAlternatively, if specific positions need truly different component types, use **multiple COMPONENT_LIST props** (e.g., `mainItem` as `COMPONENT` + `sideItems` as `COMPONENT_LIST`) instead of one list with indexed access.\n\n## `filteredComponentIds` \u2014 Restricting Allowed Children\n\nAlways set `filteredComponentIds` on `COMPONENT_LIST` props to restrict which children can be placed there. Without it, the editor allows any registered component to be placed in the slot.\n\n```json\n{\n \"name\": \"menuItems\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"my-theme-menu-link\"]\n}\n```\n\n## See Also\n- `get_framework_guide(\"component-renderer-patterns\")` \u2014 full usage patterns and examples\n- `get_framework_guide(\"private-var-map\")` \u2014 how to push parent data to children\n- `get_framework_guide(\"config-advanced-features\")` \u2014 filteredComponentIds, isHeader, etc.\n- `get_migration_guide(\"custom-data-conversion\")` \u2014 step-by-step DYNAMIC_LIST \u2192 COMPONENT_LIST migration",
|
|
202
202
|
"tags": [
|
|
203
203
|
"migration",
|
|
204
204
|
"IkasComponentRenderer",
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
"prop-runtime-shapes": {
|
|
213
213
|
"title": "Runtime Shapes of Prop Types",
|
|
214
214
|
"description": "Exact TypeScript shapes your component receives at runtime for each prop type. Covers common confusions (.data vs .links, .variant vs .product).",
|
|
215
|
-
"content": "When you declare a prop in `ikas.config.json`, the component receives a specific runtime shape. This table documents what you actually get
|
|
215
|
+
"content": "When you declare a prop in `ikas.config.json`, the component receives a specific runtime shape. This table documents what you actually get \u2014 which is often different from the editor UI suggests and from naive expectations.\n\n## Reference Table\n\n| PropType | Runtime Shape | Key Access Pattern |\n|----------|---------------|--------------------|\n| `TEXT` | `string` | `props.myText` |\n| `RICH_TEXT` | `string` (HTML) | `<div dangerouslySetInnerHTML={{ __html: props.html }} />` |\n| `NUMBER` | `number` | `props.width` (direct number, NOT `.value`) |\n| `BOOLEAN` | `boolean` | `props.enabled` |\n| `COLOR` | `string` (CSS color) | `style={{ color: props.textColor }}` |\n| `IMAGE` | `IkasImage \\| null` | `getDefaultSrc(props.image)` or `getSrc(props.image, { width: 400 })` |\n| `IMAGE_LIST` | `IkasImageList` | `props.images.map(img => getDefaultSrc(img))` |\n| `VIDEO` | `IkasVideo \\| null` | Check `.url`, `.type` (EMBED vs FILE) |\n| `LINK` | `IkasNavigationLink` | `props.link.href`, `props.link.label`, `props.link.subLinks`, `props.link.openInNewTab` |\n| `LIST_OF_LINK` | `IkasNavigationLinkList` | **`props.links.links`** \u2014 access via `.links` property (NOT `.data`) |\n| `PRODUCT` | `IkasProduct \\| null` | `props.product.name`, `.prices`, etc. |\n| `PRODUCT_LIST` | `IkasProductList` | **`props.productList.data`** \u2014 array of products via `.data` property |\n| `CATEGORY` | `IkasCategory \\| null` | `props.category.name`, `.href` |\n| `CATEGORY_LIST` | `IkasCategoryList` | Check shape with `get_type_definition(\"IkasCategoryList\")` |\n| `BRAND` / `BRAND_LIST` | `IkasBrand \\| null` / `IkasBrandList` | Use `get_model_guide(\"IkasBrand\")` |\n| `BLOG` / `BLOG_LIST` | `IkasBlog \\| null` / `IkasBlogList` | Blog list uses `.data` |\n| `PRODUCT_ATTRIBUTE` | `IkasProductAttributeValue \\| null` | Direct single value |\n| `PRODUCT_ATTRIBUTE_LIST` | `IkasProductAttributeValue[]` | Plain array (NOT wrapped) |\n| `ENUM` | `string` (the enum value) | Compare directly: `props.type === \"email\"` |\n| `COMPONENT` | `any` | Pass to `<IkasComponentRenderer components={[comp] as any[]} />` |\n| `COMPONENT_LIST` | `any[]` | Pass to `<IkasComponentRenderer components={list as any[]} />` |\n\n## Critical: Inconsistent Access Patterns\n\nThe most common mistakes come from **list-type props having inconsistent access patterns**:\n\n- `PRODUCT_LIST` \u2192 `.data` (array of products)\n- `BLOG_LIST` \u2192 `.data` (array of blogs)\n- `LIST_OF_LINK` \u2192 `.links` (NOT `.data`)\n- `PRODUCT_ATTRIBUTE_LIST` \u2192 plain array (no wrapper)\n- `IMAGE_LIST` \u2192 plain array via `.data` (check with `get_type_definition`)\n\n**When in doubt, call `get_model_guide(\"IkasProductList\")` etc. for the exact shape.**\n\n## Image Sizing: getDefaultSrc vs getSrc\n\n- `getDefaultSrc(image)` \u2014 returns the default-sized URL. Use for small images, thumbnails, or when size doesn't matter.\n- `getSrc(image, { width: 400 })` \u2014 returns a URL at a specific width. Use for performance-critical images where you know the rendered size.\n- `createMediaSrcset(image)` \u2014 generates a responsive `srcset` string for `<img srcset={...}>`.\n\n**Rule of thumb:** Use `getSrc` with a concrete width for large images; use `getDefaultSrc` for icons/small images.\n\n## Store Data Shapes (cartStore, customerStore, etc.)\n\nStore data is NOT a prop type \u2014 you import stores directly from `@ikas/bp-storefront`. Their runtime shapes are:\n\n- `cartStore.cart` \u2192 `IkasCart`: `.orderLineItems` is the array; each item has **`.variant`** (not `.product`), `.quantity`, `.totalPrice`\n- `customerStore.customer` \u2192 `IkasCustomer | null`: null when logged out, has `.firstName`, `.lastName`, `.email`, `.id`\n- `customerStore.isAuthenticated` \u2192 boolean\n\n**For full store shapes, always use `get_model_guide(\"IkasCart\")`, `get_model_guide(\"IkasCustomer\")`, etc. \u2014 do not guess.**\n\n## Old \u2192 New Runtime Shape Changes\n\nWhen migrating, remember these runtime changes:\n\n- Old `IkasSlider` (with `.value`) \u2192 New `number` (plain). Replace `width?.value` with just `width`.\n- Old `<Image>` component (renders itself) \u2192 New `getDefaultSrc(image)` + native `<img>`.\n- Old `useStore()` hook \u2192 New direct import: `import { cartStore } from '@ikas/bp-storefront'`.\n- Old `IkasAttributeDetail` / `IkasAttributeList` \u2192 New `IkasProductAttributeValue` / array.\n\n\n\n## Order Line Item Shapes (cart, order pages)\n\nThese come up when iterating over `IkasCart.orderLineItems` or `IkasOrder.orderLineItems`. They trip up LLMs because the field names don't match scalar intuition:\n\n- **`IkasOrderLineItemOption.values`** \u2014 this is an **array** of `IkasOrderLineItemOptionValue`, NOT a scalar `.value`. Iterate it with `.map`:\n\n ```tsx\n // WRONG \u2014 there is no .value on IkasOrderLineItemOption\n <span>{option.value}</span>\n\n // RIGHT \u2014 .values is an array, each item has its own .name/.value/.formattedPrice\n {option.values.map((v) => (\n <span key={v.name}>{v.name}: {v.value}</span>\n ))}\n ```\n\n- **`IkasOrderAdjustment` has no `.formattedAmount` property.** Call the helper instead: `getOrderAdjustmentFormattedAmount(adjustment)` from `@ikas/bp-storefront`. The helper returns the formatted currency string and handles the sign (negated for decrements). See `get_function_doc(\"getOrderAdjustmentFormattedAmount\")`.\n\n## See Also\n- `get_model_guide(\"<TypeName>\")` \u2014 one-stop shop for type definition + utility functions + examples\n- `get_type_definition(\"<TypeName>\")` \u2014 raw type definition\n- `get_framework_guide(\"common-pitfalls\")` \u2014 runtime shape mistakes (point #9 covers `.data`)\n- `get_framework_guide(\"imports\")` \u2014 where each type/function comes from",
|
|
216
216
|
"tags": [
|
|
217
217
|
"migration",
|
|
218
218
|
"runtime",
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
"link-prop-decision-guide": {
|
|
231
231
|
"title": "LINK Prop Decision Guide",
|
|
232
232
|
"description": "When to use LINK, LIST_OF_LINK, LINK with subLinks, or COMPONENT_LIST for link-related data. Addresses common confusion between similar prop types.",
|
|
233
|
-
"content": "Several prop types can represent \"links\" in the new system. Choosing the right one depends on the shape of the data and what the store owner should control.\n\n## Decision Tree\n\n**Is it a single link?**\n
|
|
233
|
+
"content": "Several prop types can represent \"links\" in the new system. Choosing the right one depends on the shape of the data and what the store owner should control.\n\n## Decision Tree\n\n**Is it a single link?**\n\u2192 Use `LINK`. Runtime: `IkasNavigationLink { href, label, subLinks, openInNewTab, ... }`\n\n**Is it a flat list of sibling links (no extra data per link)?**\n\u2192 Use `LIST_OF_LINK`. Runtime: `IkasNavigationLinkList` with `.links: IkasNavigationLink[]`\n\n**Is it a hierarchical menu (links with sub-links, no extra data)?**\n\u2192 Use `LIST_OF_LINK`. Each `IkasNavigationLink` already has `.subLinks: IkasNavigationLink[]`. The editor handles sub-link nesting natively.\n\n**Is it a list of links WITH extra per-link data (image, color, badge, mega-menu content)?**\n\u2192 Use `COMPONENT_LIST` with a child component that has one `LINK` prop + the extra fields.\n\n**Is it a list of products/categories/blogs (not generic links)?**\n\u2192 Use the specific prop type (`PRODUCT_LIST`, `CATEGORY_LIST`, `BLOG_LIST`), NOT `LIST_OF_LINK`.\n\n## Examples\n\n### Footer bottom bar: \"Privacy | Terms | Contact\"\nSimple flat list \u2192 `LIST_OF_LINK`:\n```json\n{ \"name\": \"bottomLinks\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.bottomLinks.links.map(link => <a href={link.href}>{link.label}</a>)`\n\n### Main nav menu with dropdowns: \"Women (\u2192 Tops, Bottoms, Shoes)\"\nHierarchical but just labels + links \u2192 `LIST_OF_LINK` (sub-links built-in):\n```json\n{ \"name\": \"menu\", \"type\": \"LIST_OF_LINK\" }\n```\nAccess: `props.menu.links.map(link => (<div>{link.label}{link.subLinks?.map(...)}</div>))`\n\n### Mega menu with images per column\nLinks PLUS image + custom layout per menu item \u2192 `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"menuItems\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-mega-menu-item\"] }\n// Child MegaMenuItem:\n{ \"props\": [\n { \"name\": \"mainLink\", \"type\": \"LINK\" },\n { \"name\": \"columnImage\", \"type\": \"IMAGE\" },\n { \"name\": \"subLinks\", \"type\": \"LIST_OF_LINK\" }\n]}\n```\n\n### Social icons: \"Instagram | Facebook | Twitter\" each with its own icon image\nList of links with an image each \u2192 `COMPONENT_LIST`:\n```json\n// Parent:\n{ \"name\": \"socials\", \"type\": \"COMPONENT_LIST\", \"filteredComponentIds\": [\"my-theme-social-link\"] }\n// Child SocialLink:\n{ \"props\": [\n { \"name\": \"link\", \"type\": \"LINK\" },\n { \"name\": \"icon\", \"type\": \"IMAGE\" }\n]}\n```\n\n## Old CUSTOM \u2192 Which Link Prop?\n\nWhen migrating an old CUSTOM/DYNAMIC_LIST:\n\n| Old customData shape | New prop |\n|----------------------|----------|\n| `OBJECT { link: LINK }` | `LINK` (flatten) |\n| `DYNAMIC_LIST of OBJECT { link: LINK }` | `LIST_OF_LINK` (if just label+href) or `COMPONENT_LIST` (if more fields) |\n| `DYNAMIC_LIST of OBJECT { link, image }` | `COMPONENT_LIST` + child component |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: LIST_OF_LINK }` | `LIST_OF_LINK` (subLinks are native) |\n| `DYNAMIC_LIST of OBJECT { mainlink, sublinks: DYNAMIC_LIST of OBJECT { links, images, cols } }` | `COMPONENT_LIST` + `MenuItem` child + nested `COMPONENT_LIST` for columns |\n\n## See Also\n- `get_migration_guide(\"component-composition-decision-guide\")` \u2014 when COMPONENT_LIST is overkill vs. repeated scalars vs. domain LIST types\n- `get_migration_guide(\"prop-runtime-shapes\")` \u2014 exact shapes for each link type\n- `get_migration_guide(\"component-renderer-limitations\")` \u2014 COMPONENT_LIST constraints\n- `get_framework_guide(\"navigation-patterns\")` \u2014 Router.navigate usage, link handling",
|
|
234
234
|
"tags": [
|
|
235
235
|
"migration",
|
|
236
236
|
"LINK",
|
|
@@ -241,6 +241,20 @@
|
|
|
241
241
|
"decision",
|
|
242
242
|
"COMPONENT_LIST"
|
|
243
243
|
]
|
|
244
|
+
},
|
|
245
|
+
"component-composition-decision-guide": {
|
|
246
|
+
"title": "When to Use COMPONENT / COMPONENT_LIST (Composition Decision Guide)",
|
|
247
|
+
"description": "Decision tree for choosing between scalar props, domain LIST types, and COMPONENT/COMPONENT_LIST. Applies to both greenfield component design and migration callouts.",
|
|
248
|
+
"content": "Default to the simplest prop shape that fits. Reach for `COMPONENT` / `COMPONENT_LIST` only when each item is a multi-field unit AND the count is dynamic. Over-used child components make themes harder to author.\n\n## Decision Tree (pick the first that matches)\n\n1. **Toggling whether to show optional pieces inside a section?** \u2192 `BOOLEAN` props on the parent (`showPrice`, `showVariants`). Do NOT wrap near-empty children in `COMPONENT_LIST` just to flip visibility.\n2. **Fixed, small count (\u22644) of items, same shape?** \u2192 repeated scalar props on the parent (`title1`/`link1`, `title2`/`link2`, \u2026).\n3. **Dynamic list where each item IS one domain object** (link, image, product, category, brand, blog, blog category, raffle)? \u2192 matching domain LIST prop type: `LIST_OF_LINK`, `IMAGE_LIST`, `PRODUCT_LIST`, `CATEGORY_LIST`, `BRAND_LIST`, `BLOG_LIST`, `BLOG_CATEGORY_LIST`, `RAFFLE_LIST`. No child component needed.\n4. **Dynamic list AND each item is a structured record (\u22653 fields, or mixed types like image+text+link+color)?** \u2192 `COMPONENT_LIST` with a dedicated child.\n5. **Exactly one structured child, not a list?** \u2192 `COMPONENT` with one child.\n\n## Sizing Rule\n\n- Child would have **0\u20132 props** \u2192 almost never `COMPONENT_LIST`. Use Branch 1, 2, or 3.\n- Child would have **3\u20134 props** \u2192 `COMPONENT_LIST` is acceptable; check whether scalars or a domain LIST type covers it first.\n- Child would have **5+ props OR mixed prop types** \u2192 `COMPONENT_LIST` is correct.\n\n## Anti-Patterns\n\n- List of items where each item is one text field \u2192 repeated `TEXT` props.\n- Wrapping 2\u20133 leaf \"display card\" components purely to toggle visibility \u2192 `BOOLEAN` props.\n- List of `{icon, link}` items \u2192 `LIST_OF_LINK` (icon derivable from link) or repeated scalars.\n\n## Sanity Check Before You Commit to `COMPONENT_LIST`\n\nAll three should hold:\n- Each child meaningfully renders its own content (its own CSS, handlers, layout).\n- Reordering items in the editor is a real UX win for the store owner.\n- The same child is reusable in multiple parents (or could be).\n\n## Migration Carve-Out\n\nThe Feature-Parity Rule still applies \u2014 do NOT silently drop fields. But if an old `customData` has only **1\u20132 fields**, prefer flattening to scalar props on the parent and log it under `## Custom Data Decisions` in `MIGRATION.md`. An explicit, logged 1\u20132-field flatten is fine; silent simplification is not. For \u22653-field records, build the component.\n\n## See Also\n\n- `get_migration_guide(\"custom-data-conversion\")` \u2014 Feature-Parity Rule + worked \u22653-field examples\n- `get_migration_guide(\"link-prop-decision-guide\")` \u2014 link-shaped data specifically\n- `get_prop_types` \u2014 full prop type reference including all domain LIST types\n",
|
|
249
|
+
"tags": [
|
|
250
|
+
"composition",
|
|
251
|
+
"COMPONENT_LIST",
|
|
252
|
+
"COMPONENT",
|
|
253
|
+
"decision-guide",
|
|
254
|
+
"greenfield",
|
|
255
|
+
"migration",
|
|
256
|
+
"simplicity"
|
|
257
|
+
]
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
|
-
}
|
|
260
|
+
}
|
package/data/storefront-api.json
CHANGED