@ikas/code-components-mcp 1.4.0-beta.173 → 1.4.0-beta.175
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 +10 -6
- package/data/storefront-api.json +137 -2
- package/data/storefront-types.json +17 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/framework.json
CHANGED
|
@@ -147,8 +147,6 @@
|
|
|
147
147
|
"live-reload",
|
|
148
148
|
"websocket",
|
|
149
149
|
"workflow",
|
|
150
|
-
"ikas theme dev",
|
|
151
|
-
"ikas theme init",
|
|
152
150
|
"tunnel",
|
|
153
151
|
"connect",
|
|
154
152
|
"routing"
|
|
@@ -245,14 +243,17 @@
|
|
|
245
243
|
"form-handling": {
|
|
246
244
|
"title": "Form Handling",
|
|
247
245
|
"description": "How to use the form model pattern for login, registration, address, and other forms",
|
|
248
|
-
"content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\n// Root export — no observer() needed, ikas runtime handles reactivity via autorun()\nexport function LoginForm() {\n // Forms are reached through get*Form(customerStore) accessors — the store\n // holds them in private fields (`_loginForm`), so `customerStore.loginForm`\n // does not compile.\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(\"/account\");\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n {loginForm.isFailure && <div>{loginForm.responseMessage}</div>}\n\n <input\n type=\"email\"\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, e.target.value)}\n />\n {loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n\n <input\n type=\"password\"\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, e.target.value)}\n />\n {loginForm.password.hasError && <span>{loginForm.password.message}</span>}\n\n <button type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n );\n}\n\nexport default LoginForm;\n```\n\n### Available Form Types\n\n| Form | Init | Setter Functions | Submit |\n|------|------|-----------------|--------|\n| Login | `initLoginForm(form)` | `setLoginFormEmail()`, `setLoginFormPassword()` | `submitLoginForm(form)` |\n| Register | `initRegisterForm(form)` | `setRegisterFormEmail()`, `setRegisterFormPassword()`, `setRegisterFormFirstName()`, `setRegisterFormLastName()` | `submitRegisterForm(form)` |\n| Forgot Password | `initForgotPasswordForm(form)` | `setForgotPasswordFormEmail()` | `submitForgotPasswordForm(form)` |\n| Address | `initAddressForm(form)` | `setAddressFormField()` for each field | `submitAddressForm(form)` |\n| Contact | `initContactForm(form)` | `setContactFormField()` for each field | `submitContactForm(form)` |\n| Account Info | `initAccountInfoForm(form)` | `setAccountInfoFormField()` for each field | `submitAccountInfoForm(form)` |\n| Newsletter | `initNewsletterSubscriptionForm(form)` | `setNewsletterEmail()` | `submitNewsletterSubscriptionForm(form)` |\n| Coupon Code | `getCouponCodeForm(customerStore)` → `initCouponCodeForm(form)` | `setCouponCodeFormCouponCode(form, value)` | `submitCouponCodeForm(form)` (remove with `removeCouponCodeForm(form)`) |\n\n**Address is the exception to this page.** It follows the same init/set/submit contract, but its\nfield set and row layout are resolved from the selected country at runtime via\n`addressForm.addressFormat` -- you cannot lay it out statically. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building any address form.\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
|
|
246
|
+
"content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\n// Root export — no observer() needed, ikas runtime handles reactivity via autorun()\nexport function LoginForm() {\n // Forms are reached through get*Form(customerStore) accessors — the store\n // holds them in private fields (`_loginForm`), so `customerStore.loginForm`\n // does not compile.\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(\"/account\");\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n {loginForm.isFailure && <div>{loginForm.responseMessage}</div>}\n\n <input\n type=\"email\"\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, e.target.value)}\n />\n {loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n\n <input\n type=\"password\"\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, e.target.value)}\n />\n {loginForm.password.hasError && <span>{loginForm.password.message}</span>}\n\n <button type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n );\n}\n\nexport default LoginForm;\n```\n\n### Available Form Types\n\n| Form | Init | Setter Functions | Submit |\n|------|------|-----------------|--------|\n| Login | `initLoginForm(form)` | `setLoginFormEmail()`, `setLoginFormPassword()` | `submitLoginForm(form)` |\n| Register | `initRegisterForm(form)` | `setRegisterFormEmail()`, `setRegisterFormPassword()`, `setRegisterFormFirstName()`, `setRegisterFormLastName()` | `submitRegisterForm(form)` |\n| Forgot Password | `initForgotPasswordForm(form)` | `setForgotPasswordFormEmail()` | `submitForgotPasswordForm(form)` |\n| Address | `initAddressForm(form)` | `setAddressFormField()` for each field | `submitAddressForm(form)` |\n| Contact | `initContactForm(form)` | `setContactFormField()` for each field | `submitContactForm(form)` |\n| Account Info | `initAccountInfoForm(form)` | `setAccountInfoFormField()` for each field | `submitAccountInfoForm(form)` |\n| Newsletter | `initNewsletterSubscriptionForm(form)` | `setNewsletterEmail()` | `submitNewsletterSubscriptionForm(form)` |\n| Coupon Code | `getCouponCodeForm(customerStore)` → `initCouponCodeForm(form)` | `setCouponCodeFormCouponCode(form, value)` | `submitCouponCodeForm(form)` (remove with `removeCouponCodeForm(form)`) |\n| Deactivate Customer | `getDeactivateCustomerForm(customerStore)` → `initDeactivateCustomerForm(form)` | `setDeactivateCustomerFormPassword(form, value)` | `submitDeactivateCustomerForm(form)` (call `logout(customerStore)` after success) |\n\n**Address is the exception to this page.** It follows the same init/set/submit contract, but its\nfield set and row layout are resolved from the selected country at runtime via\n`addressForm.addressFormat` -- you cannot lay it out statically. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building any address form.\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
|
|
249
247
|
"tags": [
|
|
250
248
|
"form",
|
|
251
249
|
"login",
|
|
252
250
|
"register",
|
|
253
251
|
"address",
|
|
254
252
|
"validation",
|
|
255
|
-
"input"
|
|
253
|
+
"input",
|
|
254
|
+
"password",
|
|
255
|
+
"account",
|
|
256
|
+
"deactivate"
|
|
256
257
|
]
|
|
257
258
|
},
|
|
258
259
|
"async-data-patterns": {
|
|
@@ -378,7 +379,7 @@
|
|
|
378
379
|
"account-patterns": {
|
|
379
380
|
"title": "Account & Dashboard Patterns",
|
|
380
381
|
"description": "Orders list, order detail, address management, favorites, and account info forms",
|
|
381
|
-
"content": "## Account & Dashboard Patterns\n\nAccount sections typically use a tab-based layout with orders, addresses, favorites, and account info.\n\n> **`customerStore` has exactly one public field: `customer`.** Everything else on the store is\n> underscore-prefixed internal state. Order and favorite lists are **not** fields on the store —\n> they come from the `Promise` that `getOrders()` / `getFavoriteProducts()` return. Reading\n> `customerStore.orders` or `customerStore.favoriteProducts` is a TypeScript error (TS2339 /\n> TS2551), not a silent empty list.\n>\n> This is the one place where copying data into `useState` is correct. The\n> \"don't mirror store data in `useState`\" rule in `async-data-patterns` §5 is about\n> store-backed observable fields; orders and favorites have no such field to observe.\n\n### Orders List\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n IkasOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n} from \"@ikas/bp-storefront\";\n\n// getOrders() RESOLVES with the orders — it writes nothing to the store.\n// Keep the result in local state; there is no `customerStore.orders`.\nconst [orders, setOrders] = useState<IkasOrder[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getOrders(customerStore)\n .then((result) => {\n if (!cancelled) setOrders(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n\n{orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <span>Order #{order.orderNumber}</span>\n <span>{getIkasOrderFormattedOrderedAt(order)}</span>\n <span>{getIkasOrderTotalItemCount(order)} items</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n <span>{getIkasOrderPackageStatusTranslation(order)}</span>\n </a>\n))}\n```\n\n### Order Detail\n\n```tsx\nimport {\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// order.orderLineItems contains line items\n{order.orderLineItems.map((item) => {\n const image = item.variant\n ? getIkasOrderLineVariantMainImage(item.variant)\n : null;\n return (\n <div key={item.id}>\n {image && <img src={getDefaultSrc(image)} />}\n <span>{item.variant?.name}</span>\n <span>Qty: {item.quantity}</span>\n </div>\n );\n})}\n```\n\n### Address Management\n\nAddress forms are country-driven: the field set and the row layout come from\n`addressForm.addressFormat`, not from a list you write. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building one.\n\n```tsx\nimport {\n getEmptyAddressForm,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n customerStore,\n} from \"@ikas/bp-storefront\";\n\nconst addresses = customerStore.customer?.addresses ?? [];\n\n// Create: getEmptyAddressForm takes the STORE.\n// Edit: getIkasCustomerAddressForm takes the ADDRESS.\nconst addressForm = address ? getIkasCustomerAddressForm(address) : getEmptyAddressForm(customerStore);\n\n// init is async and loads the country format + location lists; gate render on it.\nawait initAddressForm(addressForm, address);\n\n// Render rows from addressForm.addressFormat, set values via setAddressForm*(addressForm, value),\n// then submit (it validates and returns a boolean):\nconst ok = await submitAddressForm(addressForm);\n\n// Delete takes the ADDRESS OBJECT, not an id:\nawait deleteCustomerAddress(customerStore, address);\n```\n\n### Favorites\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\n// Same rule as orders: getFavoriteProducts() resolves with the products.\n// There is no `customerStore.favoriteProducts` field.\nconst [favorites, setFavorites] = useState<IkasProduct[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getFavoriteProducts(customerStore)\n .then((result) => {\n if (!cancelled) setFavorites(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n```\n\nThe favorites **list** is a fetched snapshot, but the favorite **flag** is store-backed and\nreactive — `isFavoriteIkasProduct(product)` reads the store synchronously, so heart icons inside\nan `observer()` sub-component update on their own:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n} from \"@ikas/bp-storefront\";\n\nconst FavoriteButton = observer(function FavoriteButton({ product }: { product: IkasProduct }) {\n const isFavorite = isFavoriteIkasProduct(product);\n return (\n <button\n onClick={() =>\n isFavorite\n ? removeIkasProductFromFavorites(product)\n : addIkasProductToFavorites(product)\n }\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n );\n});\n```\n\nBecause the list is a snapshot, removing a favorite does **not** re-render it. Drop the item from\nyour local array as well:\n\n```tsx\nonFavoriteRemove={() => setFavorites((prev) => prev.filter((f) => f.id !== product.id))}\n```\n\nNote: `getFavoriteProducts` fetches at most 100 products in one call.\n\n### Account Info Form\n\n```tsx\nimport {\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nconst accountForm = getAccountInfoForm(customerStore);\n\nuseEffect(() => {\n initAccountInfoForm(accountForm);\n}, []);\n\nconst handleSave = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n};\n```\n\n### Tab Navigation Pattern\n\nProduction themes use a tab-based layout:\n\n```tsx\nconst [activeTab, setActiveTab] = useState(\"orders\");\n\nconst tabs = [\n { id: \"orders\", label: \"Orders\" },\n { id: \"addresses\", label: \"Addresses\" },\n { id: \"favorites\", label: \"Favorites\" },\n { id: \"account\", label: \"Account Info\" },\n];\n\n<div className=\"account-tabs\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n className={activeTab === tab.id ? \"active\" : \"\"}\n onClick={() => setActiveTab(tab.id)}\n >\n {tab.label}\n </button>\n ))}\n</div>\n\n{activeTab === \"orders\" && <OrdersList />}\n{activeTab === \"addresses\" && <AddressList />}\n{activeTab === \"favorites\" && <FavoritesList />}\n{activeTab === \"account\" && <AccountInfoForm />}\n```",
|
|
382
|
+
"content": "## Account & Dashboard Patterns\n\nAccount sections typically use a tab-based layout with orders, addresses, favorites, and account info.\n\n> **`customerStore` has exactly one public field: `customer`.** Everything else on the store is\n> underscore-prefixed internal state. Order and favorite lists are **not** fields on the store —\n> they come from the `Promise` that `getOrders()` / `getFavoriteProducts()` return. Reading\n> `customerStore.orders` or `customerStore.favoriteProducts` is a TypeScript error (TS2339 /\n> TS2551), not a silent empty list.\n>\n> This is the one place where copying data into `useState` is correct. The\n> \"don't mirror store data in `useState`\" rule in `async-data-patterns` §5 is about\n> store-backed observable fields; orders and favorites have no such field to observe.\n\n### Orders List\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n IkasOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n} from \"@ikas/bp-storefront\";\n\n// getOrders() RESOLVES with the orders — it writes nothing to the store.\n// Keep the result in local state; there is no `customerStore.orders`.\nconst [orders, setOrders] = useState<IkasOrder[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getOrders(customerStore)\n .then((result) => {\n if (!cancelled) setOrders(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n\n{orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <span>Order #{order.orderNumber}</span>\n <span>{getIkasOrderFormattedOrderedAt(order)}</span>\n <span>{getIkasOrderTotalItemCount(order)} items</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n <span>{getIkasOrderPackageStatusTranslation(order)}</span>\n </a>\n))}\n```\n\n### Order Detail\n\n```tsx\nimport {\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// order.orderLineItems contains line items\n{order.orderLineItems.map((item) => {\n const image = item.variant\n ? getIkasOrderLineVariantMainImage(item.variant)\n : null;\n return (\n <div key={item.id}>\n {image && <img src={getDefaultSrc(image)} />}\n <span>{item.variant?.name}</span>\n <span>Qty: {item.quantity}</span>\n </div>\n );\n})}\n```\n\n### Address Management\n\nAddress forms are country-driven: the field set and the row layout come from\n`addressForm.addressFormat`, not from a list you write. Call\n`get_framework_guide({ topic: \"address-form-patterns\" })` before building one.\n\n```tsx\nimport {\n getEmptyAddressForm,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n customerStore,\n} from \"@ikas/bp-storefront\";\n\nconst addresses = customerStore.customer?.addresses ?? [];\n\n// Create: getEmptyAddressForm takes the STORE.\n// Edit: getIkasCustomerAddressForm takes the ADDRESS.\nconst addressForm = address ? getIkasCustomerAddressForm(address) : getEmptyAddressForm(customerStore);\n\n// init is async and loads the country format + location lists; gate render on it.\nawait initAddressForm(addressForm, address);\n\n// Render rows from addressForm.addressFormat, set values via setAddressForm*(addressForm, value),\n// then submit (it validates and returns a boolean):\nconst ok = await submitAddressForm(addressForm);\n\n// Delete takes the ADDRESS OBJECT, not an id:\nawait deleteCustomerAddress(customerStore, address);\n```\n\n### Favorites\n\n```tsx\nimport { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\n// Same rule as orders: getFavoriteProducts() resolves with the products.\n// There is no `customerStore.favoriteProducts` field.\nconst [favorites, setFavorites] = useState<IkasProduct[]>([]);\nconst [loading, setLoading] = useState(true);\n\nuseEffect(() => {\n let cancelled = false;\n getFavoriteProducts(customerStore)\n .then((result) => {\n if (!cancelled) setFavorites(result ?? []);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n}, []);\n```\n\nThe favorites **list** is a fetched snapshot, but the favorite **flag** is store-backed and\nreactive — `isFavoriteIkasProduct(product)` reads the store synchronously, so heart icons inside\nan `observer()` sub-component update on their own:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n} from \"@ikas/bp-storefront\";\n\nconst FavoriteButton = observer(function FavoriteButton({ product }: { product: IkasProduct }) {\n const isFavorite = isFavoriteIkasProduct(product);\n return (\n <button\n onClick={() =>\n isFavorite\n ? removeIkasProductFromFavorites(product)\n : addIkasProductToFavorites(product)\n }\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n );\n});\n```\n\nBecause the list is a snapshot, removing a favorite does **not** re-render it. Drop the item from\nyour local array as well:\n\n```tsx\nonFavoriteRemove={() => setFavorites((prev) => prev.filter((f) => f.id !== product.id))}\n```\n\nNote: `getFavoriteProducts` fetches at most 100 products in one call.\n\n### Account Info Form\n\n```tsx\nimport {\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nconst accountForm = getAccountInfoForm(customerStore);\n\nuseEffect(() => {\n initAccountInfoForm(accountForm);\n}, []);\n\nconst handleSave = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n};\n```\n\n### Delete Account (Deactivate Customer)\n\nAccount deletion is a form like any other — the customer confirms with their password. The form\ndoes **not** clear the session, so call `logout(customerStore)` yourself once it succeeds.\n\n```tsx\nimport {\n customerStore,\n getDeactivateCustomerForm,\n initDeactivateCustomerForm,\n setDeactivateCustomerFormPassword,\n submitDeactivateCustomerForm,\n logout,\n Router,\n} from \"@ikas/bp-storefront\";\n\nconst deactivateForm = getDeactivateCustomerForm(customerStore);\n\nuseEffect(() => {\n initDeactivateCustomerForm(deactivateForm);\n}, []);\n\nconst handleDelete = async (e: Event) => {\n e.preventDefault();\n const success = await submitDeactivateCustomerForm(deactivateForm);\n if (success) {\n await logout(customerStore);\n Router.navigateToPage(\"INDEX\");\n }\n};\n\n<form onSubmit={handleDelete}>\n {deactivateForm.isFailure && <div>{deactivateForm.responseMessage}</div>}\n\n <input\n type=\"password\"\n value={deactivateForm.password.value}\n onInput={(e) => setDeactivateCustomerFormPassword(deactivateForm, e.target.value)}\n />\n {deactivateForm.password.hasError && <span>{deactivateForm.password.message}</span>}\n\n <button type=\"submit\" disabled={deactivateForm.isSubmitting}>\n {deactivateForm.isSubmitting ? \"Deleting...\" : \"Delete Account\"}\n </button>\n</form>\n```\n\n`submit` already turns the API error for an existing deletion request into a translated\n`responseMessage` — render `responseMessage` rather than inspecting the API response yourself.\nDeletion is irreversible, so gate the form behind a confirmation step in your own UI.\n\n### Tab Navigation Pattern\n\nProduction themes use a tab-based layout:\n\n```tsx\nconst [activeTab, setActiveTab] = useState(\"orders\");\n\nconst tabs = [\n { id: \"orders\", label: \"Orders\" },\n { id: \"addresses\", label: \"Addresses\" },\n { id: \"favorites\", label: \"Favorites\" },\n { id: \"account\", label: \"Account Info\" },\n];\n\n<div className=\"account-tabs\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n className={activeTab === tab.id ? \"active\" : \"\"}\n onClick={() => setActiveTab(tab.id)}\n >\n {tab.label}\n </button>\n ))}\n</div>\n\n{activeTab === \"orders\" && <OrdersList />}\n{activeTab === \"addresses\" && <AddressList />}\n{activeTab === \"favorites\" && <FavoritesList />}\n{activeTab === \"account\" && <AccountInfoForm />}\n```",
|
|
382
383
|
"tags": [
|
|
383
384
|
"account",
|
|
384
385
|
"orders",
|
|
@@ -386,7 +387,10 @@
|
|
|
386
387
|
"favorites",
|
|
387
388
|
"dashboard",
|
|
388
389
|
"customer",
|
|
389
|
-
"tabs"
|
|
390
|
+
"tabs",
|
|
391
|
+
"deactivate",
|
|
392
|
+
"delete account",
|
|
393
|
+
"password"
|
|
390
394
|
]
|
|
391
395
|
},
|
|
392
396
|
"header-footer-patterns": {
|
package/data/storefront-api.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-24T11:34:54.069Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListProductBrand",
|
|
@@ -10746,6 +10746,88 @@
|
|
|
10746
10746
|
"isAsync": true,
|
|
10747
10747
|
"isClass": false
|
|
10748
10748
|
},
|
|
10749
|
+
{
|
|
10750
|
+
"name": "initDeactivateCustomerForm",
|
|
10751
|
+
"displayName": "init",
|
|
10752
|
+
"signature": "function initDeactivateCustomerForm(deactivateCustomerForm: DeactivateCustomerForm): void",
|
|
10753
|
+
"description": "Initializes the deactivate customer form with default password field values, labels, and placeholders.",
|
|
10754
|
+
"params": [
|
|
10755
|
+
{
|
|
10756
|
+
"name": "deactivateCustomerForm",
|
|
10757
|
+
"description": "- The deactivate customer form model to initialize"
|
|
10758
|
+
}
|
|
10759
|
+
],
|
|
10760
|
+
"returns": "void",
|
|
10761
|
+
"returnType": "void",
|
|
10762
|
+
"example": "```typescript\nimport { initDeactivateCustomerForm } from \"@ikas/bp-storefront\";\ninitDeactivateCustomerForm(deactivateCustomerForm);\n```",
|
|
10763
|
+
"categories": [
|
|
10764
|
+
"Validation",
|
|
10765
|
+
"DeactivateCustomer",
|
|
10766
|
+
"Form"
|
|
10767
|
+
],
|
|
10768
|
+
"related": [
|
|
10769
|
+
"setDeactivateCustomerFormPassword",
|
|
10770
|
+
"submitDeactivateCustomerForm"
|
|
10771
|
+
],
|
|
10772
|
+
"isAsync": false,
|
|
10773
|
+
"isClass": false
|
|
10774
|
+
},
|
|
10775
|
+
{
|
|
10776
|
+
"name": "setDeactivateCustomerFormPassword",
|
|
10777
|
+
"displayName": "setPassword",
|
|
10778
|
+
"signature": "function setDeactivateCustomerFormPassword(deactivateCustomerForm: DeactivateCustomerForm, value: string): void",
|
|
10779
|
+
"description": "Sets the password value on the deactivate customer form and re-validates if the form has already been submitted.",
|
|
10780
|
+
"params": [
|
|
10781
|
+
{
|
|
10782
|
+
"name": "deactivateCustomerForm",
|
|
10783
|
+
"description": "- The deactivate customer form model to update"
|
|
10784
|
+
},
|
|
10785
|
+
{
|
|
10786
|
+
"name": "value",
|
|
10787
|
+
"description": "- The new password string to set"
|
|
10788
|
+
}
|
|
10789
|
+
],
|
|
10790
|
+
"returns": "void",
|
|
10791
|
+
"returnType": "void",
|
|
10792
|
+
"example": "```typescript\nimport { setDeactivateCustomerFormPassword } from \"@ikas/bp-storefront\";\nsetDeactivateCustomerFormPassword(deactivateCustomerForm, \"mySecurePassword123\");\n```",
|
|
10793
|
+
"categories": [
|
|
10794
|
+
"Validation",
|
|
10795
|
+
"DeactivateCustomer",
|
|
10796
|
+
"Form"
|
|
10797
|
+
],
|
|
10798
|
+
"related": [
|
|
10799
|
+
"initDeactivateCustomerForm",
|
|
10800
|
+
"submitDeactivateCustomerForm"
|
|
10801
|
+
],
|
|
10802
|
+
"isAsync": false,
|
|
10803
|
+
"isClass": false
|
|
10804
|
+
},
|
|
10805
|
+
{
|
|
10806
|
+
"name": "submitDeactivateCustomerForm",
|
|
10807
|
+
"displayName": "submit",
|
|
10808
|
+
"signature": "async function submitDeactivateCustomerForm(deactivateCustomerForm: DeactivateCustomerForm): Promise<boolean>",
|
|
10809
|
+
"description": "Validates and submits the deactivate customer form, deleting the current customer's account after password verification.\nThe customer session is not cleared automatically, call `logout` after a successful submission.",
|
|
10810
|
+
"params": [
|
|
10811
|
+
{
|
|
10812
|
+
"name": "deactivateCustomerForm",
|
|
10813
|
+
"description": "- The deactivate customer form model to submit"
|
|
10814
|
+
}
|
|
10815
|
+
],
|
|
10816
|
+
"returns": "A promise that resolves to true if the account was deactivated successfully, or false on validation error or API failure",
|
|
10817
|
+
"returnType": "Promise<boolean>",
|
|
10818
|
+
"example": "```typescript\nimport { customerStore, logout, submitDeactivateCustomerForm } from \"@ikas/bp-storefront\";\nconst success = await submitDeactivateCustomerForm(deactivateCustomerForm);\nif (success) {\n await logout(customerStore);\n}\n```",
|
|
10819
|
+
"categories": [
|
|
10820
|
+
"Validation",
|
|
10821
|
+
"DeactivateCustomer",
|
|
10822
|
+
"Form"
|
|
10823
|
+
],
|
|
10824
|
+
"related": [
|
|
10825
|
+
"initDeactivateCustomerForm",
|
|
10826
|
+
"setDeactivateCustomerFormPassword"
|
|
10827
|
+
],
|
|
10828
|
+
"isAsync": true,
|
|
10829
|
+
"isClass": false
|
|
10830
|
+
},
|
|
10749
10831
|
{
|
|
10750
10832
|
"name": "initForgotPasswordForm",
|
|
10751
10833
|
"displayName": "init",
|
|
@@ -13986,7 +14068,8 @@
|
|
|
13986
14068
|
],
|
|
13987
14069
|
"related": [
|
|
13988
14070
|
"hasCustomer",
|
|
13989
|
-
"logout"
|
|
14071
|
+
"logout",
|
|
14072
|
+
"getDeactivateCustomerForm"
|
|
13990
14073
|
],
|
|
13991
14074
|
"isAsync": true,
|
|
13992
14075
|
"isClass": false,
|
|
@@ -14557,6 +14640,58 @@
|
|
|
14557
14640
|
"IkasCustomerStore"
|
|
14558
14641
|
]
|
|
14559
14642
|
},
|
|
14643
|
+
{
|
|
14644
|
+
"name": "getDeactivateCustomerForm",
|
|
14645
|
+
"signature": "function getDeactivateCustomerForm(customerStore: IkasCustomerStore): DeactivateCustomerForm",
|
|
14646
|
+
"description": "Get or initialize the deactivate customer form with validation for deleting the current customer's account.",
|
|
14647
|
+
"params": [
|
|
14648
|
+
{
|
|
14649
|
+
"name": "customerStore",
|
|
14650
|
+
"description": "- The customer store instance"
|
|
14651
|
+
}
|
|
14652
|
+
],
|
|
14653
|
+
"returns": "The deactivate customer form object with validated fields",
|
|
14654
|
+
"returnType": "DeactivateCustomerForm",
|
|
14655
|
+
"example": "```typescript\nimport {\n customerStore,\n getDeactivateCustomerForm,\n setDeactivateCustomerFormPassword,\n submitDeactivateCustomerForm,\n logout,\n} from \"@ikas/bp-storefront\";\n\nfunction DeleteAccountPage() {\n const form = getDeactivateCustomerForm(customerStore);\n\n const handleSubmit = async () => {\n const isSuccess = await submitDeactivateCustomerForm(form);\n if (isSuccess) {\n await logout(customerStore);\n Router.navigateToPage(\"INDEX\");\n }\n };\n\n return (\n <form>\n <input\n type=\"password\"\n value={form.password.value}\n onChange={e => setDeactivateCustomerFormPassword(form, e.target.value)}\n />\n <button onClick={handleSubmit}>Delete Account</button>\n </form>\n );\n}\n```",
|
|
14656
|
+
"categories": [
|
|
14657
|
+
"Customer",
|
|
14658
|
+
"Account"
|
|
14659
|
+
],
|
|
14660
|
+
"related": [
|
|
14661
|
+
"clearDeactivateCustomerForm",
|
|
14662
|
+
"deactivateCustomer"
|
|
14663
|
+
],
|
|
14664
|
+
"isAsync": false,
|
|
14665
|
+
"isClass": false,
|
|
14666
|
+
"parameterTypes": [
|
|
14667
|
+
"IkasCustomerStore"
|
|
14668
|
+
]
|
|
14669
|
+
},
|
|
14670
|
+
{
|
|
14671
|
+
"name": "clearDeactivateCustomerForm",
|
|
14672
|
+
"signature": "function clearDeactivateCustomerForm(customerStore: IkasCustomerStore): void",
|
|
14673
|
+
"description": "Clear the cached deactivate customer form, forcing a fresh form on next retrieval.",
|
|
14674
|
+
"params": [
|
|
14675
|
+
{
|
|
14676
|
+
"name": "customerStore",
|
|
14677
|
+
"description": "- The customer store instance"
|
|
14678
|
+
}
|
|
14679
|
+
],
|
|
14680
|
+
"returnType": "void",
|
|
14681
|
+
"example": "```typescript\nimport { customerStore, clearDeactivateCustomerForm } from \"@ikas/bp-storefront\";\n\nfunction handleDeactivateCustomerReset() {\n clearDeactivateCustomerForm(customerStore);\n}\n```",
|
|
14682
|
+
"categories": [
|
|
14683
|
+
"Customer",
|
|
14684
|
+
"Account"
|
|
14685
|
+
],
|
|
14686
|
+
"related": [
|
|
14687
|
+
"getDeactivateCustomerForm"
|
|
14688
|
+
],
|
|
14689
|
+
"isAsync": false,
|
|
14690
|
+
"isClass": false,
|
|
14691
|
+
"parameterTypes": [
|
|
14692
|
+
"IkasCustomerStore"
|
|
14693
|
+
]
|
|
14694
|
+
},
|
|
14560
14695
|
{
|
|
14561
14696
|
"name": "getForgotPasswordForm",
|
|
14562
14697
|
"signature": "function getForgotPasswordForm(customerStore: IkasCustomerStore): ForgotPasswordForm",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-24T11:34:54.098Z",
|
|
3
3
|
"types": [
|
|
4
4
|
{
|
|
5
5
|
"name": "TextElementTagTypes",
|
|
@@ -13831,6 +13831,22 @@
|
|
|
13831
13831
|
],
|
|
13832
13832
|
"filePath": "validator/customer-review/index.ts"
|
|
13833
13833
|
},
|
|
13834
|
+
{
|
|
13835
|
+
"name": "DeactivateCustomerForm",
|
|
13836
|
+
"kind": "type",
|
|
13837
|
+
"domain": "validator",
|
|
13838
|
+
"properties": [
|
|
13839
|
+
{
|
|
13840
|
+
"name": "password",
|
|
13841
|
+
"type": "IkasFormItem",
|
|
13842
|
+
"optional": false
|
|
13843
|
+
}
|
|
13844
|
+
],
|
|
13845
|
+
"extends": [
|
|
13846
|
+
"IkasForm"
|
|
13847
|
+
],
|
|
13848
|
+
"filePath": "validator/deactivate-customer/index.ts"
|
|
13849
|
+
},
|
|
13834
13850
|
{
|
|
13835
13851
|
"name": "ForgotPasswordForm",
|
|
13836
13852
|
"kind": "type",
|
package/dist/index.js
CHANGED
|
@@ -1969,7 +1969,7 @@ const server = new McpServer({
|
|
|
1969
1969
|
version: "0.1.0",
|
|
1970
1970
|
}, {
|
|
1971
1971
|
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
|
|
1972
|
-
"Live-editor actions (require `ikas
|
|
1972
|
+
"Live-editor actions (require `ikas theme dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `move_page_section` → reorder a section already placed on a page (by `direction` up/down or an absolute `to_index`) — the only way to change section order, since the editor UI offers it solely as a drag; `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
|
|
1973
1973
|
"To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
|
|
1974
1974
|
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1975
1975
|
"(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
|
|
@@ -3696,7 +3696,7 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3696
3696
|
}
|
|
3697
3697
|
//
|
|
3698
3698
|
// Tool: list_editor_pages
|
|
3699
|
-
server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas
|
|
3699
|
+
server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas theme dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
|
|
3700
3700
|
project_root: z
|
|
3701
3701
|
.string()
|
|
3702
3702
|
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
@@ -4277,7 +4277,7 @@ server.tool("get_page_by_type", 'Resolve a theme page\'s id from its pageType (C
|
|
|
4277
4277
|
return callEditorAction(project_root, args);
|
|
4278
4278
|
});
|
|
4279
4279
|
// Tool: create_page
|
|
4280
|
-
server.tool("create_page", 'Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:"PAGE", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas
|
|
4280
|
+
server.tool("create_page", 'Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:"PAGE", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas theme dev` running with the editor connected.', {
|
|
4281
4281
|
project_root: z
|
|
4282
4282
|
.string()
|
|
4283
4283
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4308,7 +4308,7 @@ server.tool("create_page", 'Create a theme page of a given pageType in the conne
|
|
|
4308
4308
|
return callEditorAction(project_root, args);
|
|
4309
4309
|
});
|
|
4310
4310
|
// Tool: publish_theme
|
|
4311
|
-
server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas
|
|
4311
|
+
server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas theme dev` running with the editor connected.", {
|
|
4312
4312
|
project_root: z
|
|
4313
4313
|
.string()
|
|
4314
4314
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4337,7 +4337,7 @@ server.tool("publish_theme", "Publish the current theme LIVE — the same action
|
|
|
4337
4337
|
const EDITOR_WORKFLOW_GUIDE = [
|
|
4338
4338
|
"# Editor workflow: placing sections and filling their content on a page",
|
|
4339
4339
|
"",
|
|
4340
|
-
"Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas
|
|
4340
|
+
"Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas theme dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
|
|
4341
4341
|
"",
|
|
4342
4342
|
"## Two distinct jobs — do not confuse them",
|
|
4343
4343
|
"- (A) DEFINE props / author component code — changing WHICH props a component has. Edits component source/config (config add-prop/add-component, index.tsx) and needs a rebuild. Do this ONLY when a needed prop does not exist yet, or the user explicitly asks to build/modify the component.",
|
|
@@ -4398,7 +4398,7 @@ server.tool("get_editor_workflow", "Read this FIRST when the user wants to place
|
|
|
4398
4398
|
};
|
|
4399
4399
|
});
|
|
4400
4400
|
// Tool: list_theme_globals
|
|
4401
|
-
server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas
|
|
4401
|
+
server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas theme dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
|
|
4402
4402
|
project_root: z
|
|
4403
4403
|
.string()
|
|
4404
4404
|
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
@@ -4490,7 +4490,7 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
|
|
|
4490
4490
|
"- breakpoint: requires name + width (a positive INTEGER, in px — no decimals). To CHANGE an existing breakpoint later, use `update_theme_breakpoint` (edits in place, keeps the id).\n" +
|
|
4491
4491
|
"- keyframe: requires name + points (array of { point, styles? }) where each style is { property, value } using a CSS property name (opacity, transform, filter, background, color, …); keyframe_type defaults to \"keyframe\". Optional `settings` = animation-level config as { property, value } entries, TYPE-DEPENDENT: for keyframe — animation-duration, animation-iteration-count, animation-play-state, animation-delay, animation-timing-function, animation-direction, animation-fill-mode, animation-timeline, animation-range, transform-origin, backface-visibility; for transition — transition, backface-visibility. Setting these means the editor drives the animation timing (you don't have to hand-write `animation:` CSS). Use the keyframe's `ref` as the CSS animation-name. For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% point is ignored (the start state is the element's own normal state). To CHANGE an existing keyframe later, use `update_theme_keyframe` (edits in place, keeps the `ref` stable) — do NOT delete+recreate, which changes the ref and breaks references.\n" +
|
|
4492
4492
|
'- colorScheme: requires name + colors (array of { slotId? | newSlotName?, value }) — each color targets ONE color SLOT, EITHER by `slotId` (an EXISTING slot\'s id, from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key — the id-keyed path) OR by `newSlotName` (a name to CREATE-or-reuse a slot; write-only, for the standard vocabulary and brand-new slots). Pass EXACTLY ONE of the two per entry. `value` is EITHER a literal color (hex/rgb/hsl or a CSS color name, e.g. "#0b1120") OR a REFERENCE to an existing theme color, passed as that color\'s cssVar from `list_theme_globals` (e.g. "var(--<colorId>)"). A `var(--id)` value is stored as a linked reference (editing the global color updates the scheme too and the dependency is tracked) — do NOT inline a hex you copied from a global color; pass its cssVar. SLOT IDENTITY: `colorSchemes.schemes` lists each slot as `{id, name}`. To set a color on an EXISTING slot, find it there by name and pass its `slotId` (names are NOT unique — two design assets can each have a "Background" — so when ambiguous disambiguate via the scheme\'s existing `colorsByScheme` keys); use `newSlotName` only to add a slot the scheme does not have yet. When creating slots use the theme\'s STANDARD slot names so components resolve them: "Background", "Heading", "Text", "Link", "HoverLink", "Border", and per button "PrimaryButton/Background", "PrimaryButton/Text", "PrimaryButton/Border", "PrimaryButton/HoverBackground", "PrimaryButton/HoverText", "PrimaryButton/HoverBorder" (and the same six under "SecondaryButton/"); a "/" groups the slot in the editor UI. A scheme should normally define EVERY slot it uses; omitted slots fall back to the default scheme\'s color. A mistaken slot can be removed with `delete_theme_global` kind `colorSchemeSlot` (by slot id), or renamed in place (id stays stable) with `rename_theme_color_scheme_slot`. To CHANGE an existing scheme later, use `update_theme_color_scheme` (edits in place, keeps the className stable; merges colors by slot, and can set/clear its default flag via `is_default`).\n' +
|
|
4493
|
-
"Read created items back with `list_theme_globals`. Requires `ikas
|
|
4493
|
+
"Read created items back with `list_theme_globals`. Requires `ikas theme dev` running with the editor connected.", {
|
|
4494
4494
|
project_root: z
|
|
4495
4495
|
.string()
|
|
4496
4496
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4662,7 +4662,7 @@ server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE
|
|
|
4662
4662
|
"point is ignored (the start state is the element's own normal state). " +
|
|
4663
4663
|
"Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
|
|
4664
4664
|
"see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
|
|
4665
|
-
"Requires `ikas
|
|
4665
|
+
"Requires `ikas theme dev` running with the editor connected.", {
|
|
4666
4666
|
project_root: z
|
|
4667
4667
|
.string()
|
|
4668
4668
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4725,7 +4725,7 @@ server.tool("update_text_style", "Update an existing theme typography token (tex
|
|
|
4725
4725
|
"this text style the theme's default/global style (the editor's right-click \"Make Default Style\" / \"Global " +
|
|
4726
4726
|
"Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
|
|
4727
4727
|
"current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
|
|
4728
|
-
"`ikas
|
|
4728
|
+
"`ikas theme dev` running with the editor connected.", {
|
|
4729
4729
|
project_root: z
|
|
4730
4730
|
.string()
|
|
4731
4731
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4802,7 +4802,7 @@ server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN P
|
|
|
4802
4802
|
"(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
|
|
4803
4803
|
"because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
4804
4804
|
"Pass `width` (px) to change it; omit it to only rename. " +
|
|
4805
|
-
"Requires `ikas
|
|
4805
|
+
"Requires `ikas theme dev` running with the editor connected.", {
|
|
4806
4806
|
project_root: z
|
|
4807
4807
|
.string()
|
|
4808
4808
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4849,7 +4849,7 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
|
|
|
4849
4849
|
"fallback whose colors fill any slot a non-default scheme leaves unset); this is exclusive — the editor " +
|
|
4850
4850
|
"automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
|
|
4851
4851
|
"one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
|
|
4852
|
-
"is_default to no-op. Requires `ikas
|
|
4852
|
+
"is_default to no-op. Requires `ikas theme dev` running with the editor connected.", {
|
|
4853
4853
|
project_root: z
|
|
4854
4854
|
.string()
|
|
4855
4855
|
.describe("Absolute path to the code-component project."),
|
|
@@ -4897,7 +4897,7 @@ server.tool("rename_theme_color_scheme_slot", 'Rename a color scheme SLOT (a sch
|
|
|
4897
4897
|
'because slot names can collide across design assets. `name` is the NEW slot name (a "/" groups it in the ' +
|
|
4898
4898
|
'editor UI, e.g. "PrimaryButton/Background"). Only non-design-asset (user/theme) slots can be renamed here. ' +
|
|
4899
4899
|
"This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
|
|
4900
|
-
"scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas
|
|
4900
|
+
"scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas theme dev` running with the " +
|
|
4901
4901
|
"editor connected.", {
|
|
4902
4902
|
project_root: z
|
|
4903
4903
|
.string()
|