webcake-storefront-mcp 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/attributes.js +2 -0
- package/dist/builder/bindings.js +253 -0
- package/dist/builder/catalog.js +14 -1
- package/dist/builder/events.js +152 -0
- package/dist/builder/guide.js +26 -5
- package/dist/builder/page.js +14 -6
- package/dist/changelog.json +7 -7
- package/dist/tools/builder.js +4 -0
- package/package.json +1 -1
|
@@ -335,6 +335,8 @@ export function describeAttributes(type, skeleton) {
|
|
|
335
335
|
notes: curated.notes,
|
|
336
336
|
common_specials_note: "custom_class/custom_css/element_async/event_name_custom work on almost every element.",
|
|
337
337
|
layout_hint: "runtime.style holds CSS (width/height/color/fontSize/background/border…); runtime.config holds grid placement (see get_build_guide). On save these expand into bp1..bp4.",
|
|
338
|
+
events_hint: "Attach interactions via opts.events (ids auto-minted). Call list_events for the trigger/action catalog + required fields.",
|
|
339
|
+
bindings_hint: "Attach dynamic data via opts.bindings (ids auto-minted). Call list_bindings for the dataset/field catalog. Inside a repeater (grid-product/cart-items/post-list…) children bind to the per-item dataset.",
|
|
338
340
|
coverage: obs.seen ? `${obs.seen} real instances mined` : "factory default only",
|
|
339
341
|
};
|
|
340
342
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Authoritative BINDINGS catalog + helpers for the BuilderX component model.
|
|
2
|
+
//
|
|
3
|
+
// A dataset element (text-dataset, image-dataset, …) and the children of a repeater
|
|
4
|
+
// (grid-product, cart-items, post-list, …) pull LIVE data via a `bindings` array. Each
|
|
5
|
+
// binding is a FLAT object:
|
|
6
|
+
// { id, name, target, ...metadata }
|
|
7
|
+
// - name = the dataset/source ("product", "cart_item", "order", "post", …)
|
|
8
|
+
// - target = "<name>::<field>" (e.g. "product::product_price")
|
|
9
|
+
// Source of truth: builderx_spa/src/composable/bind.js (runtime resolver),
|
|
10
|
+
// builderx_spa/src/components/editor/traits/{BindingSetting,ConnectData}.vue (the field
|
|
11
|
+
// menus), builderx_api/assets/render/*-binding.js (storefront resolve). A binding only
|
|
12
|
+
// resolves on a page whose `type` enables the dataset (product/cart → use_store, etc).
|
|
13
|
+
// This module mirrors that so the MCP can MINT, VALIDATE and TEACH valid bindings.
|
|
14
|
+
import { randomString } from "./factory.js";
|
|
15
|
+
// Curated to the fields an AI realistically uses, lifted verbatim from the editor field
|
|
16
|
+
// menus. (The full lists run longer; these cover storefront page building.)
|
|
17
|
+
export const BINDING_DATASETS = {
|
|
18
|
+
product: {
|
|
19
|
+
name: "product",
|
|
20
|
+
page_type: "store",
|
|
21
|
+
repeater_parents: ["grid-product", "slider-product", "custom-layout", "layout-dataset"],
|
|
22
|
+
summary: "A product (detail page, or per-item inside a product grid/slider).",
|
|
23
|
+
fields: [
|
|
24
|
+
"product::product_name", "product::product_price", "product::product_original_price",
|
|
25
|
+
"product::product_image", "product::product_url", "product::short_description",
|
|
26
|
+
"product::product_categories", "product::brand_name", "product::product_tag",
|
|
27
|
+
"product::product_remain_quantity", "product::total_sold_web", "product::rating_point",
|
|
28
|
+
"product::rating_count", "product::variation_sku", "product::product_wholesale_price",
|
|
29
|
+
"product::discount_price_from_original_price", "product::preorder_text", "product::product_note",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
"product-overlay": {
|
|
33
|
+
name: "product-overlay",
|
|
34
|
+
page_type: "store",
|
|
35
|
+
repeater_parents: ["grid-product", "slider-product"],
|
|
36
|
+
summary: "Sale/discount badge overlaid on a product card.",
|
|
37
|
+
fields: ["product-overlay::product_sale", "product-overlay::product_discount", "product-overlay::product_total_sold", "product-overlay::product_url"],
|
|
38
|
+
},
|
|
39
|
+
cart_item: {
|
|
40
|
+
name: "cart_item",
|
|
41
|
+
page_type: "store",
|
|
42
|
+
repeater_parents: ["cart-items"],
|
|
43
|
+
summary: "A line item in the cart, plus cart totals.",
|
|
44
|
+
fields: [
|
|
45
|
+
"cart_item::cart_item_image", "cart_item::cart_item_name", "cart_item::cart_item_price",
|
|
46
|
+
"cart_item::cart_item_original_price", "cart_item::cart_item_subtotal", "cart_item::cart_item_total_price",
|
|
47
|
+
"cart_item::cart_item_total_quantity", "cart_item::cart_item_prod_attr", "cart_item::sku",
|
|
48
|
+
"cart_item::cart_total_price", "cart_item::cart_total_tax", "cart_item::cart_shipping_fee",
|
|
49
|
+
"cart_item::cart_promotion_discount", "cart_item::subtotal_before_discount", "cart_item::product_note",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
order: {
|
|
53
|
+
name: "order",
|
|
54
|
+
page_type: "store",
|
|
55
|
+
summary: "The order summary (checkout / thank-you page).",
|
|
56
|
+
fields: [
|
|
57
|
+
"order::order_full_name", "order::order_phone_number", "order::order_email", "order::address",
|
|
58
|
+
"order::order_payment_method", "order::shipping_fee", "order::subtotal", "order::total_price",
|
|
59
|
+
"order::total_tax", "order::code_order", "order::order_status", "order::payment_status",
|
|
60
|
+
"order::order_quantity", "order::order_date", "order::order_coupon", "order::order_promotion_discount",
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
order_item: {
|
|
64
|
+
name: "order_item",
|
|
65
|
+
page_type: "store",
|
|
66
|
+
repeater_parents: ["order-items"],
|
|
67
|
+
summary: "A line item inside an order summary.",
|
|
68
|
+
fields: [
|
|
69
|
+
"order_item::product_name", "order_item::product_price", "order_item::original_price",
|
|
70
|
+
"order_item::product_image", "order_item::product_attrs", "order_item::product_quantity",
|
|
71
|
+
"order_item::items_sum_up_price", "order_item::tag_bonus", "order_item::preorder_text",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
post: {
|
|
75
|
+
name: "post",
|
|
76
|
+
page_type: "blog",
|
|
77
|
+
repeater_parents: ["post-list", "slider-post", "grid-blog", "slider-blog"],
|
|
78
|
+
summary: "A blog article/post (list item or detail).",
|
|
79
|
+
fields: [
|
|
80
|
+
"post::post_title", "post::post_description", "post::post_content", "post::post_image",
|
|
81
|
+
"post::post_publish_date", "post::category_name", "post::category_description", "post::category_image",
|
|
82
|
+
"post::post_creator", "post::tag_article", "post::total_views",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
category: {
|
|
86
|
+
name: "category",
|
|
87
|
+
page_type: "store",
|
|
88
|
+
repeater_parents: ["grid-category", "slider-category"],
|
|
89
|
+
summary: "A product category (per-item inside a category grid/slider).",
|
|
90
|
+
fields: ["category::category_name", "category::category_image", "category::category_description"],
|
|
91
|
+
},
|
|
92
|
+
customer: {
|
|
93
|
+
name: "customer",
|
|
94
|
+
page_type: "member",
|
|
95
|
+
summary: "The logged-in customer profile + loyalty.",
|
|
96
|
+
fields: [
|
|
97
|
+
"customer::profile_avatar", "customer::profile_name", "customer::phone_number", "customer::email",
|
|
98
|
+
"customer::gender", "customer::birthday", "customer::order_count", "customer::purchased_amount",
|
|
99
|
+
"customer::pos_reward_point", "customer::pos_reward_point_level", "customer::pos_level_discount",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
customer_address: {
|
|
103
|
+
name: "customer_address",
|
|
104
|
+
page_type: "member",
|
|
105
|
+
repeater_parents: ["customer-address"],
|
|
106
|
+
summary: "A saved customer address (member page).",
|
|
107
|
+
fields: [
|
|
108
|
+
"customer_address::full_name", "customer_address::first_name", "customer_address::last_name",
|
|
109
|
+
"customer_address::phone_number", "customer_address::address", "customer_address::province",
|
|
110
|
+
"customer_address::district", "customer_address::commune", "customer_address::pdc", "customer_address::is_default",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
bonus_item: {
|
|
114
|
+
name: "bonus_item",
|
|
115
|
+
page_type: "store",
|
|
116
|
+
repeater_parents: ["bonus-items"],
|
|
117
|
+
summary: "A combo/gift item (promotion bonus).",
|
|
118
|
+
fields: [
|
|
119
|
+
"bonus_item::bonus_item_name", "bonus_item::bonus_item_price", "bonus_item::bonus_item_original_price",
|
|
120
|
+
"bonus_item::bonus_item_image", "bonus_item::bonus_item_prod_attr", "bonus_item::bonus_item_quantity",
|
|
121
|
+
"bonus_item::tag_bonus", "bonus_item::combo_name", "bonus_item::sku",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
promotion_item: {
|
|
125
|
+
name: "promotion_item",
|
|
126
|
+
page_type: "store",
|
|
127
|
+
repeater_parents: ["promotions", "promotions-short"],
|
|
128
|
+
summary: "A promotion in a promotions list.",
|
|
129
|
+
fields: ["promotion_item::name", "promotion_item::code", "promotion_item::image", "promotion_item::description", "promotion_item::end_date"],
|
|
130
|
+
},
|
|
131
|
+
attr: {
|
|
132
|
+
name: "attr",
|
|
133
|
+
page_type: "store",
|
|
134
|
+
summary: "A product attribute (variation axis) name/value.",
|
|
135
|
+
fields: ["attr::attr_name", "attr::attr_value"],
|
|
136
|
+
},
|
|
137
|
+
form: {
|
|
138
|
+
name: "form",
|
|
139
|
+
page_type: "any",
|
|
140
|
+
summary: "Computed form values (coupon price, reward points).",
|
|
141
|
+
fields: ["form::coupon_price", "form::send_value", "form::total_reward_point", "form::use_reward_point", "form::remaining_reward_point", "form::submit_errors"],
|
|
142
|
+
},
|
|
143
|
+
general_info: {
|
|
144
|
+
name: "general_info",
|
|
145
|
+
page_type: "any",
|
|
146
|
+
summary: "Page / site info.",
|
|
147
|
+
fields: ["general_info::page_name", "general_info::site_name", "general_info::menu_title"],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
// Build fast lookups.
|
|
151
|
+
const ALL_TARGETS = new Set();
|
|
152
|
+
const NAME_SET = new Set();
|
|
153
|
+
for (const ds of Object.values(BINDING_DATASETS)) {
|
|
154
|
+
NAME_SET.add(ds.name);
|
|
155
|
+
for (const f of ds.fields)
|
|
156
|
+
ALL_TARGETS.add(f);
|
|
157
|
+
}
|
|
158
|
+
// Repeater parent type → the PRIMARY dataset its children bind to per-item. First write
|
|
159
|
+
// wins so the primary dataset (product) claims grid-product over secondary overlays.
|
|
160
|
+
export const REPEATER_CONTEXT = {};
|
|
161
|
+
for (const ds of Object.values(BINDING_DATASETS)) {
|
|
162
|
+
for (const p of ds.repeater_parents || [])
|
|
163
|
+
if (!REPEATER_CONTEXT[p])
|
|
164
|
+
REPEATER_CONTEXT[p] = ds.name;
|
|
165
|
+
}
|
|
166
|
+
export function isKnownBindingName(name) {
|
|
167
|
+
return NAME_SET.has(name);
|
|
168
|
+
}
|
|
169
|
+
export function isKnownBindingTarget(target) {
|
|
170
|
+
return ALL_TARGETS.has(target);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mint a structurally-valid binding. Accepts either a full target ("product::product_price")
|
|
174
|
+
* or a name + field, plus any extra metadata (show_tax, separator, name_style, …).
|
|
175
|
+
* makeBinding("product::product_price", { show_tax: true })
|
|
176
|
+
* makeBinding({ name: "cart_item", field: "cart_item_name" })
|
|
177
|
+
*/
|
|
178
|
+
export function makeBinding(spec, extra = {}) {
|
|
179
|
+
let name;
|
|
180
|
+
let target;
|
|
181
|
+
if (typeof spec === "string") {
|
|
182
|
+
target = spec;
|
|
183
|
+
name = spec.includes("::") ? spec.split("::")[0] : spec;
|
|
184
|
+
}
|
|
185
|
+
else if (spec && typeof spec === "object") {
|
|
186
|
+
name = spec.name;
|
|
187
|
+
target = spec.target || (spec.name && spec.field ? `${spec.name}::${spec.field}` : undefined);
|
|
188
|
+
if (!name && target && target.includes("::"))
|
|
189
|
+
name = target.split("::")[0];
|
|
190
|
+
const { id, name: _n, target: _t, field: _f, ...rest } = spec;
|
|
191
|
+
extra = { ...rest, ...extra };
|
|
192
|
+
if (id)
|
|
193
|
+
extra.id = id;
|
|
194
|
+
}
|
|
195
|
+
if (!name)
|
|
196
|
+
throw new Error("Binding needs a name or a 'name::field' target.");
|
|
197
|
+
const { id: keepId, ...meta } = extra;
|
|
198
|
+
return { id: keepId || `BINDING-${randomString(6)}`, name, ...(target ? { target } : {}), ...meta };
|
|
199
|
+
}
|
|
200
|
+
/** Ensure every binding in an array has an id + a name (mint where missing). */
|
|
201
|
+
export function normalizeBindings(bindings) {
|
|
202
|
+
if (!Array.isArray(bindings))
|
|
203
|
+
return bindings;
|
|
204
|
+
return bindings.map((b) => {
|
|
205
|
+
if (!b || typeof b !== "object")
|
|
206
|
+
return b;
|
|
207
|
+
if (b.id && b.name)
|
|
208
|
+
return b; // already well-formed
|
|
209
|
+
return makeBinding(b);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Validate one element's bindings against the catalog. */
|
|
213
|
+
export function validateBindings(node) {
|
|
214
|
+
const errors = [];
|
|
215
|
+
const warnings = [];
|
|
216
|
+
const where = node?.id || node?.type || "?";
|
|
217
|
+
for (const b of node?.bindings || []) {
|
|
218
|
+
if (!b || typeof b !== "object") {
|
|
219
|
+
warnings.push(`Binding on "${where}" is not an object.`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!b.name) {
|
|
223
|
+
warnings.push(`Binding on "${where}" has no name (dataset).`);
|
|
224
|
+
}
|
|
225
|
+
else if (!isKnownBindingName(b.name)) {
|
|
226
|
+
warnings.push(`Binding on "${where}" has unknown dataset "${b.name}" (see list_bindings).`);
|
|
227
|
+
}
|
|
228
|
+
if (b.target) {
|
|
229
|
+
if (!/^[a-z_-]+::.+/i.test(b.target)) {
|
|
230
|
+
warnings.push(`Binding on "${where}" target "${b.target}" is not "name::field" shaped.`);
|
|
231
|
+
}
|
|
232
|
+
else if (!isKnownBindingTarget(b.target)) {
|
|
233
|
+
warnings.push(`Binding on "${where}" target "${b.target}" is not a known field (see list_bindings).`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { errors, warnings };
|
|
238
|
+
}
|
|
239
|
+
/** Catalog for the list_bindings discovery tool. */
|
|
240
|
+
export function describeBindingsCatalog() {
|
|
241
|
+
return {
|
|
242
|
+
note: "Dataset elements (text-dataset/image-dataset/…) and children of a repeater (grid-product, cart-items, post-list…) carry bindings:[{ id, name, target }]. Build via new_element opts.bindings (ids auto-minted) — e.g. new_element('text-dataset',{ bindings:[{ target:'product::product_price', show_tax:true }] }). A target only resolves on a page whose type enables its dataset (store→use_store, member→use_member, blog→use_blog) — build_page handles the flag.",
|
|
243
|
+
page_type_required: { store: ["product", "product-overlay", "cart_item", "order", "order_item", "category", "bonus_item", "promotion_item", "attr"], member: ["customer", "customer_address"], blog: ["post"], any: ["form", "general_info"] },
|
|
244
|
+
repeater_context: REPEATER_CONTEXT,
|
|
245
|
+
datasets: Object.values(BINDING_DATASETS).map((ds) => ({
|
|
246
|
+
name: ds.name,
|
|
247
|
+
page_type: ds.page_type,
|
|
248
|
+
summary: ds.summary,
|
|
249
|
+
...(ds.repeater_parents ? { used_inside: ds.repeater_parents } : {}),
|
|
250
|
+
fields: ds.fields,
|
|
251
|
+
})),
|
|
252
|
+
};
|
|
253
|
+
}
|
package/dist/builder/catalog.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// so an AI agent can understand the palette, and (3) expose helpers the MCP tools use.
|
|
7
7
|
import * as F from "./factory.js";
|
|
8
8
|
import { describeAttributes } from "./attributes.js";
|
|
9
|
+
import { normalizeBindings } from "./bindings.js";
|
|
10
|
+
import { normalizeEvents } from "./events.js";
|
|
9
11
|
// Probe every factory once with safe default opts to learn the type string it produces
|
|
10
12
|
// and whether it is a container (has a children array). Calling with {children:[]} is
|
|
11
13
|
// safe for all 132 factories (verified) — none throw.
|
|
@@ -154,7 +156,18 @@ export function buildElement(type, opts = {}) {
|
|
|
154
156
|
throw err;
|
|
155
157
|
}
|
|
156
158
|
// Guarantee children-using factories never throw on a missing children array.
|
|
157
|
-
|
|
159
|
+
const node = fn({ children: [], ...opts });
|
|
160
|
+
// Some factories ignore opts.bindings/events — attach them so any element the AI passes
|
|
161
|
+
// them to gets them — then normalize so each binding/event has a valid id (+ name/eventName).
|
|
162
|
+
if (opts.bindings && !node.bindings)
|
|
163
|
+
node.bindings = opts.bindings;
|
|
164
|
+
if (opts.events && !node.events)
|
|
165
|
+
node.events = opts.events;
|
|
166
|
+
if (Array.isArray(node.bindings) && node.bindings.length)
|
|
167
|
+
node.bindings = normalizeBindings(node.bindings);
|
|
168
|
+
if (Array.isArray(node.events) && node.events.length)
|
|
169
|
+
node.events = normalizeEvents(node.events);
|
|
170
|
+
return node;
|
|
158
171
|
}
|
|
159
172
|
export function isKnownType(type) {
|
|
160
173
|
return Boolean(FACTORY_BY_TYPE[type]);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Authoritative EVENTS catalog + helpers for the BuilderX component model.
|
|
2
|
+
//
|
|
3
|
+
// An interactive element carries an `events: [ <event>, ... ]` array. Each event is a
|
|
4
|
+
// FLAT object the storefront renderer consumes:
|
|
5
|
+
// { id, eventName, action, ...action-specific fields }
|
|
6
|
+
// - eventName = the TRIGGER (click/hover/success/...). Source of truth:
|
|
7
|
+
// builderx_spa/src/components/editor/traits/{Events,EventSetting}.vue and the Elixir
|
|
8
|
+
// renderer builderx_api/lib/qwik/html/common.ex (validates eventName) +
|
|
9
|
+
// builderx_api/assets/render/events.js (dispatches `action` → PascalCase handler).
|
|
10
|
+
// - action = WHAT happens; each action reads its own extra fields (open_page_id,
|
|
11
|
+
// toggle_id, popup_id, link_target, …).
|
|
12
|
+
// This module mirrors that shape so the MCP can MINT valid events, VALIDATE them, and
|
|
13
|
+
// TEACH the AI the exact trigger/action/field names (no invented keys).
|
|
14
|
+
import { randomString } from "./factory.js";
|
|
15
|
+
export const EVENT_TRIGGERS = [
|
|
16
|
+
{ name: "click", when: "User clicks the element (the default for buttons/text/images/containers).", applies_to: "most elements" },
|
|
17
|
+
{ name: "hover", when: "Pointer hovers the element — used for hover style actions (scale, change_background, …).", applies_to: "most elements" },
|
|
18
|
+
{ name: "mouseenter", when: "Pointer enters the element." },
|
|
19
|
+
{ name: "mouseleave", when: "Pointer leaves the element." },
|
|
20
|
+
{ name: "success", when: "A form submitted successfully — fire follow-up actions (redirect/popup).", applies_to: "form" },
|
|
21
|
+
{ name: "submit", when: "A submit-button submits its parent form.", applies_to: "submit-button" },
|
|
22
|
+
{ name: "tab", when: "A swiper/tab changed.", applies_to: "swiper, tabs" },
|
|
23
|
+
{ name: "hide", when: "A popup is hidden.", applies_to: "popup" },
|
|
24
|
+
{ name: "onenter", when: "Enter key pressed in a search input.", applies_to: "input-search" },
|
|
25
|
+
];
|
|
26
|
+
const TRIGGER_NAMES = new Set(EVENT_TRIGGERS.map((t) => t.name));
|
|
27
|
+
export const EVENT_ACTIONS = [
|
|
28
|
+
// navigation
|
|
29
|
+
{ action: "open_page", trigger: "click", category: "navigation", summary: "Go to a page in this site.", required: ["open_page_id"], optional: ["scrollTarget", "scroll_to_id", "scroll_to_element_id", "scrollMore", "no_follow", "active_color"] },
|
|
30
|
+
{ action: "open_link", trigger: "click", category: "navigation", summary: "Open an external URL.", required: ["link_target"], optional: ["link_target_url", "no_follow"] },
|
|
31
|
+
{ action: "open_category", trigger: "click", category: "navigation", summary: "Open a product category page.", required: ["open_category_id"], optional: ["no_follow"] },
|
|
32
|
+
{ action: "open_blog_category", trigger: "click", category: "navigation", summary: "Open a blog category page.", required: ["open_blog_category_id"], optional: ["no_follow"] },
|
|
33
|
+
{ action: "scroll_to", trigger: "click", category: "navigation", summary: "Smooth-scroll to a section/element on this page.", required: ["scroll_to_id"], optional: ["scroll_to_element_id", "scrollMore", "scrollTarget"], target_in_page: ["scroll_to_id", "scroll_to_element_id"] },
|
|
34
|
+
{ action: "direction_login", trigger: "click", category: "navigation", summary: "Redirect by login state: open_page_id if logged in, target_direction_id if not.", required: ["open_page_id"], optional: ["target_direction_id"] },
|
|
35
|
+
// ui control
|
|
36
|
+
{ action: "toggle", trigger: "click", category: "ui", summary: "Show/hide another element on this page.", required: ["toggle_id"], optional: ["toggle_status", "only_mode"], target_in_page: ["toggle_id"] },
|
|
37
|
+
{ action: "open_popup", trigger: "click", category: "ui", summary: "Open a popup.", required: ["popup_id"], optional: ["popup_overlay", "close_all_other_popup"] },
|
|
38
|
+
{ action: "close_popup", trigger: "click", category: "ui", summary: "Close a popup.", required: ["popup_id"], optional: ["popup_overlay", "close_all_other_popup"] },
|
|
39
|
+
{ action: "open_menu", trigger: "click", category: "ui", summary: "Open a dropdown/submenu.", required: ["open_menu_id"], target_in_page: ["open_menu_id"] },
|
|
40
|
+
{ action: "close_menu", trigger: "click", category: "ui", summary: "Close a menu.", optional: ["close_menu_id"] },
|
|
41
|
+
{ action: "change_tab", trigger: "click", category: "ui", summary: "Switch a swiper/tabs to a given tab.", required: ["change_tab_id"], optional: ["move_to", "tab_index", "tab_color"], target_in_page: ["change_tab_id"] },
|
|
42
|
+
{ action: "load_more", trigger: "click", category: "ui", summary: "Load more items in a list.", optional: ["load_more_id"], target_in_page: ["load_more_id"] },
|
|
43
|
+
// cart / commerce
|
|
44
|
+
{ action: "add_to_cart", trigger: "click", category: "cart", summary: "Add the current product to the cart.", optional: ["open_page", "auto_add_bonus", "activeNotify", "productNoteElId"] },
|
|
45
|
+
{ action: "buy_now", trigger: "click", category: "cart", summary: "Add to cart and go straight to checkout.", optional: ["open_page", "auto_add_bonus", "activeNotify"] },
|
|
46
|
+
{ action: "add_to_cart_form", trigger: "success", category: "cart", summary: "Add to cart from a form submit (product form).", optional: ["open_page"] },
|
|
47
|
+
{ action: "open_cart", trigger: "click", category: "cart", summary: "Open the cart sidebar/drawer." },
|
|
48
|
+
{ action: "close_cart", trigger: "click", category: "cart", summary: "Close the cart sidebar/drawer." },
|
|
49
|
+
{ action: "apply_promotion", trigger: "click", category: "cart", summary: "Apply a promotion/coupon code.", optional: ["apply_id", "is_hidden", "has_text", "text_change"] },
|
|
50
|
+
// form / account
|
|
51
|
+
{ action: "required_login", trigger: "click", category: "account", summary: "Require login before proceeding (redirect to a page or popup).", optional: ["login_target", "login_page_id", "login_popup_id"] },
|
|
52
|
+
{ action: "logout", trigger: "click", category: "account", summary: "Log the customer out." },
|
|
53
|
+
{ action: "login_google", trigger: "click", category: "account", summary: "Sign in with Google." },
|
|
54
|
+
{ action: "login_facebook", trigger: "click", category: "account", summary: "Sign in with Facebook." },
|
|
55
|
+
// content
|
|
56
|
+
{ action: "change_text", trigger: "click", category: "content", summary: "Replace the text of another element.", required: ["target_id"], optional: ["text_value"], target_in_page: ["target_id"] },
|
|
57
|
+
{ action: "see_more", trigger: "click", category: "content", summary: "Expand a clamped/collapsed element.", optional: ["elements"] },
|
|
58
|
+
{ action: "shorten", trigger: "click", category: "content", summary: "Collapse a previously expanded element.", optional: ["elements"] },
|
|
59
|
+
{ action: "copy", trigger: "click", category: "content", summary: "Copy text to the clipboard.", optional: ["copy_type", "copy_data"] },
|
|
60
|
+
{ action: "download", trigger: "click", category: "content", summary: "Download a file.", required: ["download_link"], optional: ["fileType"] },
|
|
61
|
+
// contact
|
|
62
|
+
{ action: "phone_call", trigger: "click", category: "contact", summary: "Start a phone call (tel:).", required: ["phone_call_number"] },
|
|
63
|
+
{ action: "open_email", trigger: "click", category: "contact", summary: "Open the mail client (mailto:).", required: ["open_email"] },
|
|
64
|
+
{ action: "send_messenger", trigger: "click", category: "contact", summary: "Open Facebook Messenger.", optional: ["messenger_link"] },
|
|
65
|
+
{ action: "send_zalo_mess", trigger: "click", category: "contact", summary: "Open Zalo chat.", optional: ["zalo_oa_id"] },
|
|
66
|
+
{ action: "send_whatsapp_mess", trigger: "click", category: "contact", summary: "Open WhatsApp chat.", optional: ["whatsapp_phone_number"] },
|
|
67
|
+
{ action: "sharing", trigger: "click", category: "contact", summary: "Share to a social network.", optional: ["shareTarget", "shareLink", "shareLinkCustom"] },
|
|
68
|
+
// hover styles (eventName must be "hover")
|
|
69
|
+
{ action: "scale", trigger: "hover", category: "hover", summary: "Scale the element on hover.", optional: ["el_target_id"] },
|
|
70
|
+
{ action: "change_background", trigger: "hover", category: "hover", summary: "Change background colour on hover." },
|
|
71
|
+
{ action: "change_text_color", trigger: "hover", category: "hover", summary: "Change text colour on hover." },
|
|
72
|
+
{ action: "change_border_color", trigger: "hover", category: "hover", summary: "Change border colour on hover." },
|
|
73
|
+
];
|
|
74
|
+
const ACTION_BY_NAME = {};
|
|
75
|
+
for (const a of EVENT_ACTIONS)
|
|
76
|
+
ACTION_BY_NAME[a.action] = a;
|
|
77
|
+
export function isKnownTrigger(name) {
|
|
78
|
+
return TRIGGER_NAMES.has(name);
|
|
79
|
+
}
|
|
80
|
+
export function getEventAction(action) {
|
|
81
|
+
return ACTION_BY_NAME[action];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mint a structurally-valid event object. Pass at least `action`; extra action-specific
|
|
85
|
+
* fields are merged verbatim. The id and a sensible eventName are filled in.
|
|
86
|
+
* makeEvent({ action: "scroll_to", scroll_to_id: "SECTION-x" })
|
|
87
|
+
* makeEvent({ action: "open_link", eventName: "click", link_target: "https://…" })
|
|
88
|
+
*/
|
|
89
|
+
export function makeEvent(spec) {
|
|
90
|
+
if (!spec || typeof spec !== "object")
|
|
91
|
+
throw new Error("Event spec must be an object with at least an `action`.");
|
|
92
|
+
const def = spec.action ? ACTION_BY_NAME[spec.action] : undefined;
|
|
93
|
+
const eventName = spec.eventName || (def ? def.trigger : "click");
|
|
94
|
+
const { eventName: _e, action: _a, id: _id, ...rest } = spec;
|
|
95
|
+
return { id: spec.id || `EVENT-${randomString(6)}`, eventName, ...(spec.action ? { action: spec.action } : {}), ...rest };
|
|
96
|
+
}
|
|
97
|
+
/** Ensure every event in an array has an id + an eventName (mint where missing). */
|
|
98
|
+
export function normalizeEvents(events) {
|
|
99
|
+
if (!Array.isArray(events))
|
|
100
|
+
return events;
|
|
101
|
+
return events.map((e) => (e && typeof e === "object" ? makeEvent(e) : e));
|
|
102
|
+
}
|
|
103
|
+
/** Validate one element's events against the catalog + the set of ids present in the page. */
|
|
104
|
+
export function validateEvents(node, allIds) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const where = node?.id || node?.type || "?";
|
|
108
|
+
for (const ev of node?.events || []) {
|
|
109
|
+
if (!ev || typeof ev !== "object") {
|
|
110
|
+
warnings.push(`Event on "${where}" is not an object.`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ev.eventName && !isKnownTrigger(ev.eventName)) {
|
|
114
|
+
warnings.push(`Event on "${where}" has unknown eventName "${ev.eventName}" (see list_events triggers).`);
|
|
115
|
+
}
|
|
116
|
+
if (!ev.action) {
|
|
117
|
+
warnings.push(`Event on "${where}" has no action — it does nothing.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const def = ACTION_BY_NAME[ev.action];
|
|
121
|
+
if (!def) {
|
|
122
|
+
warnings.push(`Event on "${where}" has unknown action "${ev.action}" (see list_events).`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const f of def.required || []) {
|
|
126
|
+
if (ev[f] == null || ev[f] === "")
|
|
127
|
+
warnings.push(`Event "${ev.action}" on "${where}" is missing required field "${f}".`);
|
|
128
|
+
}
|
|
129
|
+
for (const f of def.target_in_page || []) {
|
|
130
|
+
const v = ev[f];
|
|
131
|
+
if (v && !allIds.has(v))
|
|
132
|
+
warnings.push(`Event "${ev.action}" on "${where}" targets missing element "${v}" (field ${f}).`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { errors, warnings };
|
|
136
|
+
}
|
|
137
|
+
/** Catalog for the list_events discovery tool. */
|
|
138
|
+
export function describeEventsCatalog() {
|
|
139
|
+
return {
|
|
140
|
+
note: "An interactive node carries events:[{ id, eventName, action, ...fields }]. eventName = trigger, action = what happens. Build with new_element opts.events (ids are auto-minted) — e.g. new_element('button',{ text:'Mua', events:[{ action:'add_to_cart', open_page:'cart' }] }).",
|
|
141
|
+
triggers: EVENT_TRIGGERS,
|
|
142
|
+
actions: EVENT_ACTIONS.map((a) => ({
|
|
143
|
+
action: a.action,
|
|
144
|
+
category: a.category,
|
|
145
|
+
usual_trigger: a.trigger,
|
|
146
|
+
summary: a.summary,
|
|
147
|
+
...(a.required ? { required: a.required } : {}),
|
|
148
|
+
...(a.optional ? { optional: a.optional } : {}),
|
|
149
|
+
...(a.target_in_page ? { references_element_on_page: a.target_in_page } : {}),
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
}
|
package/dist/builder/guide.js
CHANGED
|
@@ -92,14 +92,35 @@ key — only bp1..bp4.)
|
|
|
92
92
|
- Form: wrap inputs in a \`form\`; set \`form.specials.type\`
|
|
93
93
|
(form_order | form_login | form_signup | form_discount | order_tracking). Each input
|
|
94
94
|
needs \`specials.field_name\`.
|
|
95
|
-
- Dataset elements (text-dataset, image-dataset, rectangle-dataset
|
|
96
|
-
a
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
- Dataset elements (text-dataset, image-dataset, rectangle-dataset…) and the CHILDREN of
|
|
96
|
+
a repeater (grid-product, cart-items, order-items, post-list, grid-category, customer-address)
|
|
97
|
+
pull live data via a \`bindings\` array. Each binding is
|
|
98
|
+
\`{ id, name:<dataset>, target:"<dataset>::<field>" }\` — you DON'T set the id, the builder
|
|
99
|
+
mints it. Just pass \`opts.bindings:[{ target:"product::product_price" }]\` to new_element.
|
|
100
|
+
Common targets (call \`list_bindings\` for the full catalog — use these EXACTLY, there is no \`product::price\`):
|
|
101
|
+
- product: \`product::product_image\`, \`product::product_name\`, \`product::product_price\`, \`product::product_original_price\`, \`product::short_description\`
|
|
99
102
|
- cart_item: \`cart_item::cart_item_image\`, \`cart_item::cart_item_name\`, \`cart_item::cart_item_price\`, \`cart_item::cart_item_total_price\`, \`cart_item::cart_item_prod_attr\`
|
|
100
103
|
- order_item: \`order_item::product_image\`, \`order_item::product_name\`, \`order_item::product_quantity\`, \`order_item::items_sum_up_price\`, \`order_item::product_attrs\`
|
|
101
104
|
- customer_address: \`customer_address::full_name\`, \`customer_address::phone_number\`, \`customer_address::address\`, \`customer_address::pdc\`
|
|
102
|
-
|
|
105
|
+
REPEATER CONTEXT: inside a \`grid-product\` each cell IS a product, so a child
|
|
106
|
+
\`text-dataset\` with \`product::product_name\` resolves to that cell's product (no extra
|
|
107
|
+
wiring). Same for cart-items→cart_item, order-items→order_item, post-list→post,
|
|
108
|
+
grid-category→category. A target only resolves on a page of the matching \`type\` (below).
|
|
109
|
+
|
|
110
|
+
## Events (clicks, navigation, cart, popups)
|
|
111
|
+
Interactive nodes (button, text, image, container, rectangle, icons) carry an \`events\`
|
|
112
|
+
array. Each event is \`{ id, eventName, action, ...fields }\` — you set \`action\` (+ its
|
|
113
|
+
fields); the builder mints the id and picks a sensible \`eventName\` (trigger). Pass via
|
|
114
|
+
\`opts.events\`. Call \`list_events\` for the full trigger/action catalog. Most-used:
|
|
115
|
+
- Navigate: \`{ action:"open_page", open_page_id:"<page id>" }\`, \`{ action:"open_link", link_target:"https://…", link_target_url:"_blank" }\`, \`{ action:"open_category", open_category_id:"<id>" }\`.
|
|
116
|
+
- Scroll on this page: \`{ action:"scroll_to", scroll_to_id:"<section id on this page>" }\`.
|
|
117
|
+
- Show/hide: \`{ action:"toggle", toggle_id:"<element id on this page>" }\`, \`{ action:"open_popup", popup_id:"<popup id>" }\`.
|
|
118
|
+
- Commerce: \`{ action:"add_to_cart", open_page:"cart" }\`, \`{ action:"buy_now" }\`, \`{ action:"apply_promotion" }\`.
|
|
119
|
+
- Contact: \`{ action:"phone_call", phone_call_number:"+84…" }\`, \`{ action:"open_email", open_email:"hi@shop.vn" }\`.
|
|
120
|
+
- Hover style (set eventName:"hover"): \`{ eventName:"hover", action:"scale" }\`.
|
|
121
|
+
Example: \`new_element("button", { text:"Mua ngay", style:{…}, events:[{ action:"add_to_cart", open_page:"cart" }] })\`.
|
|
122
|
+
validate_page warns on unknown actions, missing required fields, and events whose
|
|
123
|
+
in-page target (toggle_id/scroll_to_id/…) doesn't exist on the page.
|
|
103
124
|
|
|
104
125
|
## Page types & data sources (IMPORTANT for special pages)
|
|
105
126
|
A page's \`type\` decides which live data it can bind to. A SPECIAL page only works if the
|
package/dist/builder/page.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// produce that structure the same way the builder does, so generated pages render.
|
|
7
7
|
import { buildElement, isKnownType, ELEMENT_TYPES } from "./catalog.js";
|
|
8
8
|
import { randomString } from "./factory.js";
|
|
9
|
+
import { validateEvents } from "./events.js";
|
|
10
|
+
import { validateBindings } from "./bindings.js";
|
|
9
11
|
import { BREAKPOINTS, genGridByBp, SECTION_CONTENT_COL_START, SECTION_CONTENT_COL_END, } from "./grid.js";
|
|
10
12
|
const clone = (o) => structuredClone(o);
|
|
11
13
|
/** Walk every node in a source tree (depth-first). Return false from fn to stop. */
|
|
@@ -155,13 +157,19 @@ export function validatePage(source) {
|
|
|
155
157
|
warnings.push(`Form field "${node.id}" (${node.type}) has no specials.field_name.`);
|
|
156
158
|
}
|
|
157
159
|
});
|
|
158
|
-
// Second pass:
|
|
160
|
+
// Second pass: events + bindings against the authoritative catalogs (unknown
|
|
161
|
+
// trigger/action/dataset, missing required fields, dangling in-page event targets,
|
|
162
|
+
// unknown binding fields). These are warnings — they don't block a save.
|
|
159
163
|
walk(source, (node) => {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
if (node.events && node.events.length) {
|
|
165
|
+
const r = validateEvents(node, allIds);
|
|
166
|
+
errors.push(...r.errors);
|
|
167
|
+
warnings.push(...r.warnings);
|
|
168
|
+
}
|
|
169
|
+
if (node.bindings && node.bindings.length) {
|
|
170
|
+
const r = validateBindings(node);
|
|
171
|
+
errors.push(...r.errors);
|
|
172
|
+
warnings.push(...r.warnings);
|
|
165
173
|
}
|
|
166
174
|
});
|
|
167
175
|
return {
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.14.0",
|
|
4
|
+
"d": "24/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "New list_events tool returns the authoritative interaction-events catalog: 9 triggers (click, hover, submit, success, ...) and 38 actions…",
|
|
7
|
+
"vi": "Tool mới list_events trả về danh mục sự kiện tương tác đầy đủ: 9 trigger (click, hover, submit, success, ...) và 38 action (open_page, scroll_to,…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.13.0",
|
|
4
11
|
"d": "24/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "New create_site_from_template tool clones a marketplace template (all its pages, page-sources, and settings) into a new account-owned site, switches…",
|
|
35
42
|
"vi": "Tool mới create_site_from_template nhân bản một template marketplace (toàn bộ trang, page-source và cài đặt) thành một site mới thuộc tài khoản hiện…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.10.0",
|
|
39
|
-
"d": "24/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "New scaffold_store_pages tool creates the standard storefront pages (Category, Product, Cart, Checkout, Thank-you) so navigation links resolve…",
|
|
42
|
-
"vi": "Tool mới scaffold_store_pages tạo các trang storefront tiêu chuẩn (Category, Product, Cart, Checkout, Thank-you) để các liên kết điều hướng không bị…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/tools/builder.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BUILD_GUIDE } from "../builder/guide.js";
|
|
3
3
|
import { listElements, getElement, buildElement } from "../builder/catalog.js";
|
|
4
|
+
import { describeEventsCatalog } from "../builder/events.js";
|
|
5
|
+
import { describeBindingsCatalog } from "../builder/bindings.js";
|
|
4
6
|
import { buildSection, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
|
|
5
7
|
// Recursive spec for new_section / build_page children.
|
|
6
8
|
const elementSpec = z.object({
|
|
@@ -34,6 +36,8 @@ export function registerBuilderTools(server, api, handle) {
|
|
|
34
36
|
server.tool("get_element", "Get the full detail of an element type: category, container flag, summary, an ATTRIBUTES reference (the meaningful specials/config keys + their purpose/allowed values, events, and dataset binding targets), and a live skeleton node (the authoritative default shape) you can copy and edit. Read this before authoring/editing an element so you set the right keys.", {
|
|
35
37
|
type: z.string().describe("Element type, e.g. 'text', 'button', 'grid-product'"),
|
|
36
38
|
}, ({ type }) => handle(async () => getElement(type)));
|
|
39
|
+
server.tool("list_events", "List every interaction EVENT you can attach to a node: the triggers (eventName: click/hover/success/submit/…) and the actions (open_page, scroll_to, toggle, open_popup, add_to_cart, buy_now, phone_call, open_link, …) with the exact extra fields each action needs. Attach via new_element/new_section opts.events (ids are auto-minted, e.g. opts.events=[{ action:'add_to_cart', open_page:'cart' }]).", {}, () => handle(async () => describeEventsCatalog()));
|
|
40
|
+
server.tool("list_bindings", "List every dynamic-data BINDING target: the datasets (product, cart_item, order, order_item, post, category, customer, customer_address, …) and their exact field names ('product::product_price', …), which page type each needs (store/member/blog), and how repeater children (grid-product, cart-items, post-list) bind per-item. Attach via new_element opts.bindings (ids auto-minted, e.g. opts.bindings=[{ target:'product::product_price' }]).", {}, () => handle(async () => describeBindingsCatalog()));
|
|
37
41
|
server.tool("new_element", "Build a single structurally-valid element node from the real builder factory. Returns the node — edit its specials/style, then place it in a section's children.", {
|
|
38
42
|
type: z.string().describe("Element type (see list_elements)"),
|
|
39
43
|
opts: z.record(z.any()).optional().describe("Factory opts: { text, src, width, height, style, config, specials, events }"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
6
|
"license": "MIT",
|