webcake-storefront-mcp 1.27.0 → 1.29.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/guide.js
CHANGED
|
@@ -237,10 +237,13 @@ create the globals — they embed into each page's source. If you later overwrit
|
|
|
237
237
|
later with update_global_section_element(s) and it updates on every page at once.
|
|
238
238
|
|
|
239
239
|
## Popups (newsletter / promo / age-gate)
|
|
240
|
-
A popup is a GLOBAL SOURCE, not a page section.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
A popup is a GLOBAL SOURCE, not a page section. FAST PATH: \`scaffold_popup({ brand, headline, offer })\`
|
|
241
|
+
builds + saves a designed newsletter popup (heading + text + email form + close button, centred)
|
|
242
|
+
and returns its id. To build one by hand:
|
|
243
|
+
1. Build the popup node: new_element("popup", { children:[…heading,text,form,close button…], style:{ width:480, background:"#fff", borderRadius:"12px" }, config:{ popupHorizontalPosition:"center", popupVerticalPosition:"center" }, specials:{ effect:"fade-in", timeAnim:0.5 } }).
|
|
244
|
+
- SIZE + POSITION live in the popup's runtime style/config (width/height/background + popupHorizontalPosition/popupVerticalPosition), NOT in specials. createPopup seeds a centred-modal default.
|
|
245
|
+
- TRIGGER (auto-open) lives in SPECIALS: \`openPopupAction:"openPopupWithTime"\` + \`timeOpenPopup:<seconds>\` for a delay; \`page_ids:[…]\` to limit which pages it shows on; \`effect\`/\`timeAnim\` for the animation. (There is no exit-intent/only-once flag in the node — those are app settings.)
|
|
246
|
+
- OVERLAY is NOT on the popup node — it's a field on the open_popup EVENT (popup_overlay:true).
|
|
244
247
|
2. Save it: create_global_source({ component:"popup", source:{ sections:[<popupNode>] } }) -> returns its id.
|
|
245
248
|
3. Open/close from any element via events: a button { action:"open_popup", popup_id:"<id>", popup_overlay:true };
|
|
246
249
|
a close button inside { action:"close_popup", popup_id:"<id>" }; a form can auto-close on its
|
package/dist/builder/page.js
CHANGED
|
@@ -192,7 +192,7 @@ export function buildSection(childSpecs = [], sectionOpts = {}) {
|
|
|
192
192
|
};
|
|
193
193
|
return section;
|
|
194
194
|
}
|
|
195
|
-
function buildFromSpec(spec) {
|
|
195
|
+
export function buildFromSpec(spec) {
|
|
196
196
|
if (!spec || !spec.type) {
|
|
197
197
|
throw new Error("Each element spec must have a 'type'.");
|
|
198
198
|
}
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
// Colours use the site theme CSS vars by default (var(--color_02) accent, var(--color_00)
|
|
14
14
|
// text) so a generated site matches whatever palette the theme already defines; callers can
|
|
15
15
|
// override any slot via a Palette object.
|
|
16
|
-
import { buildSection, walk } from "./page.js";
|
|
16
|
+
import { buildSection, buildFromSpec, stackChildren, walk } from "./page.js";
|
|
17
17
|
import { buildElement } from "./catalog.js";
|
|
18
18
|
import { normalizeEvents } from "./events.js";
|
|
19
|
+
const X_ICON_SVG = "<svg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'><path d='M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z'></path></svg>";
|
|
19
20
|
// Slots map onto the site theme's 5×5 colour matrix, surfaced as CSS vars var(--color_RC)
|
|
20
21
|
// (R=row, C=col). Row 0 is greyscale (00=white … 04=black); row 2 is the BRAND row. Using
|
|
21
22
|
// var(--color_00) for text was a bug — that's WHITE (invisible on the white surface).
|
|
@@ -310,7 +311,9 @@ export function headerSection(opts = {}) {
|
|
|
310
311
|
{ label: "Sản phẩm", navTo: "collections" },
|
|
311
312
|
{ label: "Giỏ hàng", navTo: "cart" },
|
|
312
313
|
];
|
|
313
|
-
const logo =
|
|
314
|
+
const logo = opts.logo
|
|
315
|
+
? { type: "image", opts: { config: { src: opts.logo }, style: { height: 40, width: 140 } } }
|
|
316
|
+
: { type: "text", opts: { text: opts.brand || "Shop", specials: { tag: "h2" }, style: { fontSize: "24px", fontWeight: "800", color: p.text } } };
|
|
314
317
|
// Real designer templates use a `menu` of `menu-item`s, NOT text links — and a menu-item's
|
|
315
318
|
// navigation lives in its SPECIALS (linkType/linkPage/pageId), not events. We build the
|
|
316
319
|
// items directly (via buildElement, so buildFromSpec doesn't grid-stack the menu) and leave
|
|
@@ -326,16 +329,32 @@ export function headerSection(opts = {}) {
|
|
|
326
329
|
children: menuItems,
|
|
327
330
|
},
|
|
328
331
|
};
|
|
332
|
+
// Right-hand actions: optional search / account / language, then cart + CTA — like real headers.
|
|
333
|
+
const actionChildren = [];
|
|
334
|
+
const actionWidths = [];
|
|
335
|
+
if (opts.search !== false) {
|
|
336
|
+
actionChildren.push({ type: "input-search", opts: { specials: { field_name: "search", placeholder: "Tìm kiếm sản phẩm...", showIcon: true }, config: { inputBackground: p.surfaceAlt, inputBorderColor: p.border, inputBorderRadius: "8px", iconColor: p.muted, placeholderColor: p.muted, iconAlign: "left" }, style: { height: 40 } } });
|
|
337
|
+
actionWidths.push({ unit: "px", absValue: 200 });
|
|
338
|
+
}
|
|
339
|
+
if (opts.account) {
|
|
340
|
+
actionChildren.push({ type: "member-bar", opts: { specials: { showView: "not_logged", layoutShow: "text", textLogin: "Đăng nhập", textSignup: "Đăng ký" }, config: { colorIcon: p.text }, style: { color: p.text } } });
|
|
341
|
+
actionWidths.push({ unit: "max-c" });
|
|
342
|
+
}
|
|
343
|
+
if (opts.language) {
|
|
344
|
+
actionChildren.push({ type: "language-menu", opts: { specials: { show_flag: true, type: "dropdown" }, config: { color: p.text, display: "abbreviated" } } });
|
|
345
|
+
actionWidths.push({ unit: "max-c" });
|
|
346
|
+
}
|
|
347
|
+
actionChildren.push({ type: "cart-icon", opts: { style: { width: 26, height: 26, color: p.text } } });
|
|
348
|
+
actionWidths.push({ unit: "px", absValue: 32 });
|
|
349
|
+
actionChildren.push(cta(opts.cta || "Đặt mua ngay", p, { navTo: "collections", height: 44 }));
|
|
350
|
+
actionWidths.push({ unit: "max-c" });
|
|
329
351
|
const actions = {
|
|
330
352
|
type: "container",
|
|
331
353
|
layout: "row",
|
|
332
354
|
columnGap: 16,
|
|
333
|
-
colWidths:
|
|
334
|
-
collapse: { bp4: 2 },
|
|
335
|
-
children:
|
|
336
|
-
{ type: "cart-icon", opts: { style: { width: 26, height: 26, color: p.text } } },
|
|
337
|
-
cta(opts.cta || "Đặt mua ngay", p, { navTo: "collections", height: 44 }),
|
|
338
|
-
],
|
|
355
|
+
colWidths: actionWidths,
|
|
356
|
+
collapse: { bp4: Math.min(2, actionChildren.length) },
|
|
357
|
+
children: actionChildren,
|
|
339
358
|
};
|
|
340
359
|
return buildSection([
|
|
341
360
|
{
|
|
@@ -392,6 +411,48 @@ export function footerSection(opts = {}) {
|
|
|
392
411
|
});
|
|
393
412
|
}
|
|
394
413
|
// ---------------------------------------------------------------------------
|
|
414
|
+
// POPUP (newsletter / promo) — a global SOURCE, not a page section
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
/**
|
|
417
|
+
* A designed newsletter/promo popup, returned as a ready-to-save global-source source
|
|
418
|
+
* `{ sections: [ <popup node> ] }`. The popup carries an auto-open-after-delay trigger in
|
|
419
|
+
* specials and a centred-modal geometry (seeded by createPopup); the close button references
|
|
420
|
+
* the popup's own id. Save with create_global_source({ component:"popup", source }).
|
|
421
|
+
* Run finalizeForRender(source) BEFORE saving (the tool does this).
|
|
422
|
+
*/
|
|
423
|
+
export function newsletterPopupSource(opts = {}) {
|
|
424
|
+
const p = resolvePalette(opts.palette);
|
|
425
|
+
const heading = {
|
|
426
|
+
type: "text",
|
|
427
|
+
opts: { text: opts.headline || "Nhận ưu đãi 10%", specials: { tag: "h2" }, style: { fontSize: "26px", fontWeight: "800", color: p.text, textAlign: "center", lineHeight: "1.3" } },
|
|
428
|
+
};
|
|
429
|
+
const sub = {
|
|
430
|
+
type: "text",
|
|
431
|
+
opts: { text: opts.subtext || "Đăng ký nhận tin để không bỏ lỡ khuyến mãi mới nhất.", style: { fontSize: "15px", color: p.muted, textAlign: "center", lineHeight: "1.6" } },
|
|
432
|
+
};
|
|
433
|
+
const form = {
|
|
434
|
+
type: "form",
|
|
435
|
+
opts: { specials: { type: "subscribe" }, config: { backgroundInput: p.surface, placeholderColor: p.muted, labelColor: p.text, textPadding: 14, rowGap: 12 } },
|
|
436
|
+
children: [
|
|
437
|
+
{ type: "email", opts: { specials: { field_name: "email", placeholder: "Email của bạn", show_label: false }, config: { backgroundInput: p.surface, textPadding: 14 }, style: { borderColor: p.border, borderRadius: "8px", height: 46 } } },
|
|
438
|
+
{ type: "submit-button", opts: { text: opts.ctaLabel || "Đăng ký ngay", style: { background: p.accent, color: p.onAccent, borderRadius: "8px", height: 46, fontWeight: "600", width: "100%", textAlign: "center" } } },
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
// Close button: a small X in the top-right that closes this popup (wired to the popup id below).
|
|
442
|
+
const close = { type: "rectangle", opts: { config: { mask: X_ICON_SVG }, style: { width: 22, height: 22, background: p.muted } } };
|
|
443
|
+
const content = [heading, sub, form, close].map((s) => buildFromSpec(s));
|
|
444
|
+
const popup = buildElement("popup", {
|
|
445
|
+
specials: { effect: "fade-in", timeAnim: 0.5, openPopupAction: "openPopupWithTime", timeOpenPopup: opts.delaySeconds ?? 6 },
|
|
446
|
+
style: { width: 460, background: p.surface, borderRadius: "14px", paddingTop: 40, paddingBottom: 40, paddingLeft: 32, paddingRight: 32 },
|
|
447
|
+
config: { popupHorizontalPosition: "center", popupVerticalPosition: "center" },
|
|
448
|
+
});
|
|
449
|
+
stackChildren(popup, content, { rowGap: 18 });
|
|
450
|
+
// Wire the close button to dismiss THIS popup now that we know its id.
|
|
451
|
+
const closeNode = content[content.length - 1];
|
|
452
|
+
closeNode.events = normalizeEvents([{ action: "close_popup", popup_id: popup.id }], "rectangle");
|
|
453
|
+
return { sections: [popup] };
|
|
454
|
+
}
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
395
456
|
// registry the scaffolder iterates
|
|
396
457
|
// ---------------------------------------------------------------------------
|
|
397
458
|
/**
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.29.0",
|
|
4
|
+
"d": "26/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "scaffold_global_sections now accepts a logo parameter (hosted image URL) that renders an image element in the header instead of the brand-name text…",
|
|
7
|
+
"vi": "scaffold_global_sections nay nhận thêm tham số logo (URL hình ảnh được host) để hiển thị phần tử image trong header thay vì node text tên thương hiệu."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.28.0",
|
|
11
|
+
"d": "26/06/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "New scaffold_popup tool builds and saves a designed newsletter/promo popup (heading + subtext + email subscribe form + close button, centred modal)…",
|
|
14
|
+
"vi": "Tool mới scaffold_popup xây dựng và lưu một popup newsletter/promo được thiết kế sẵn (tiêu đề + phụ đề + form đăng ký email + nút đóng, dạng modal…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.27.0",
|
|
4
18
|
"d": "26/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Changed",
|
|
27
41
|
"en": "list_elements now documents 16 previously-uncovered element types, completing curated attribute coverage across all 132 factory types: popup…",
|
|
28
42
|
"vi": "list_elements nay tài liệu hóa 16 kiểu phần tử trước đây chưa được ghi lại, hoàn thiện độ phủ thuộc tính curated cho toàn bộ 132 kiểu factory: popup…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.24.0",
|
|
32
|
-
"d": "26/06/2026",
|
|
33
|
-
"type": "Changed",
|
|
34
|
-
"en": "list_elements now documents tabIndex and text_animation_type as shared specials present on every element, covering tab-panel visibility binding and…",
|
|
35
|
-
"vi": "list_elements nay tài liệu hóa tabIndex và text_animation_type là các specials dùng chung có trên mọi phần tử, bao gồm gắn kết hiển thị/ẩn cho…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.23.0",
|
|
39
|
-
"d": "25/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "get_build_guide now includes a category-page filter recipe explaining how sidebar filter widgets (checkbox-group, color-group, tags,…",
|
|
42
|
-
"vi": "get_build_guide nay bổ sung công thức filter trang danh mục, giải thích cách các widget lọc trong sidebar (checkbox-group, color-group, tags,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -135,11 +135,15 @@ force=true. Two-step safety: dry_run=true (default) previews; dry_run=false perf
|
|
|
135
135
|
links: z.array(z.object({ label: z.string(), navTo: z.string().optional(), url: z.string().optional() })).optional().describe("Header nav links. navTo = a page slug ('home','collections','cart') auto-wired to open_page; url = external link. Defaults to Home/Products/Cart."),
|
|
136
136
|
contact: z.object({ phone: z.string().optional(), email: z.string().optional(), address: z.string().optional() }).optional().describe("Real contact info for the footer (don't invent — omit what you don't have)."),
|
|
137
137
|
cta: z.string().optional().describe("Header call-to-action button label (default 'Đặt mua ngay')."),
|
|
138
|
+
logo: z.string().optional().describe("Logo image URL (hosted) — renders an image instead of the brand text."),
|
|
139
|
+
search: z.boolean().default(true).describe("Add a storefront search box to the header (most real templates have one)."),
|
|
140
|
+
account: z.boolean().default(false).describe("Add a login/account bar (member sites)."),
|
|
141
|
+
language: z.boolean().default(false).describe("Add a language switcher (multilingual sites)."),
|
|
138
142
|
palette: z.record(z.any()).optional().describe("Optional colour overrides { accent, onAccent, text, muted, surface, surfaceAlt, border }."),
|
|
139
143
|
include: z.enum(["both", "header", "footer"]).default("both").describe("Which chrome to generate."),
|
|
140
144
|
force: z.boolean().default(false).describe("Create even if a header/footer global already exists (otherwise that slot is skipped)."),
|
|
141
145
|
dry_run: z.boolean().default(true).describe("Preview (true) or perform the atomic save (false)."),
|
|
142
|
-
}, ({ brand, links, contact, cta, palette, include, force, dry_run }) => handle(async () => {
|
|
146
|
+
}, ({ brand, links, contact, cta, logo, search, account, language, palette, include, force, dry_run }) => handle(async () => {
|
|
143
147
|
// Which slots already have a global section? (dedupe by type unless force)
|
|
144
148
|
const existingRes = await api.listGlobalSections().catch(() => null);
|
|
145
149
|
const existing = (existingRes && (existingRes.data || existingRes.global_sections || existingRes)) || [];
|
|
@@ -174,7 +178,7 @@ force=true. Two-step safety: dry_run=true (default) previews; dry_run=false perf
|
|
|
174
178
|
if (!force && slotTaken("header"))
|
|
175
179
|
skipped.push("header");
|
|
176
180
|
else
|
|
177
|
-
toCreate.push({ type: "header", name: "Header", node: buildNode("header", headerSection({ brand, links: links, cta, palette })) });
|
|
181
|
+
toCreate.push({ type: "header", name: "Header", node: buildNode("header", headerSection({ brand, links: links, cta, logo, search, account, language, palette })) });
|
|
178
182
|
}
|
|
179
183
|
if (wantFooter) {
|
|
180
184
|
if (!force && slotTaken("footer"))
|
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { getConfirmMode } from "./context.js";
|
|
3
3
|
import { normalizeEvents } from "../builder/events.js";
|
|
4
4
|
import { normalizeBindings } from "../builder/bindings.js";
|
|
5
|
+
import { newsletterPopupSource, contrastSafePalette } from "../builder/templates.js";
|
|
6
|
+
import { finalizeForRender, validatePage } from "../builder/page.js";
|
|
5
7
|
/**
|
|
6
8
|
* Global Sources tools — manage site-wide components: cart, popup, overview, etc.
|
|
7
9
|
*
|
|
@@ -575,6 +577,35 @@ Same merge rules: style/config/specials = shallow merge, events/bindings = repla
|
|
|
575
577
|
invalidateGsCache();
|
|
576
578
|
return res;
|
|
577
579
|
}));
|
|
580
|
+
server.tool("scaffold_popup", `Build AND save a designed newsletter/promo popup in one call (a 'popup' global source).
|
|
581
|
+
Returns the popup id — open it from any element with an open_popup event { popup_id, popup_overlay:true },
|
|
582
|
+
or set it to auto-open after a delay (built in). Centred modal: heading + subtext + email subscribe
|
|
583
|
+
form + close button, palette-aware (accent derived from the site theme). Use dry_run to preview.`, {
|
|
584
|
+
headline: z.string().optional().describe("Popup heading (default: 'Nhận ưu đãi 10%')"),
|
|
585
|
+
subtext: z.string().optional().describe("Supporting line under the heading"),
|
|
586
|
+
cta_label: z.string().optional().describe("Submit button label (default: 'Đăng ký ngay')"),
|
|
587
|
+
delay_seconds: z.number().optional().describe("Auto-open after N seconds (default 6)"),
|
|
588
|
+
palette: z.record(z.any()).optional().describe("Colour overrides { accent, text, surface, ... }; defaults to the site theme."),
|
|
589
|
+
dry_run: z.boolean().default(true).describe("Preview the popup source (true) or create+save it (false)"),
|
|
590
|
+
}, ({ headline, subtext, cta_label, delay_seconds, palette, dry_run }) => handle(async () => {
|
|
591
|
+
const themePal = await contrastSafePalette(api);
|
|
592
|
+
const source = newsletterPopupSource({
|
|
593
|
+
headline, subtext, ctaLabel: cta_label, delaySeconds: delay_seconds,
|
|
594
|
+
palette: { ...themePal, ...(palette || {}) },
|
|
595
|
+
});
|
|
596
|
+
const validation = validatePage(source);
|
|
597
|
+
finalizeForRender(source);
|
|
598
|
+
const popupId = source.sections[0]?.id || null;
|
|
599
|
+
if (dry_run) {
|
|
600
|
+
return { dry_run: true, valid: validation.valid, popup_id: popupId, ...(validation.errors ? { errors: validation.errors } : {}), hint: "Centred newsletter popup. Call again with dry_run=false to save it, then open it via an open_popup event (popup_id above)." };
|
|
601
|
+
}
|
|
602
|
+
if (!validation.valid)
|
|
603
|
+
return { error: "Popup failed validation — not saving.", validation };
|
|
604
|
+
const res = await api.createGlobalSource({ component: "popup", source, type: "default", site_id: api.siteId });
|
|
605
|
+
invalidateGsCache();
|
|
606
|
+
const id = res?.data?.id || res?.id || popupId;
|
|
607
|
+
return { success: true, global_source_id: id, popup_id: popupId, next_step: "Open it from a button/header with events:[{ action:'open_popup', popup_id, popup_overlay:true }] (or it auto-opens after the delay). publish_site to take it live." };
|
|
608
|
+
}));
|
|
578
609
|
server.tool("update_global_source", `Replace full source of a global source.
|
|
579
610
|
IMPORTANT: Before calling this tool, you MUST:
|
|
580
611
|
1. Read existing source with get_global_source_detail first
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.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",
|