webcake-landing-mcp 1.0.51 → 1.0.52
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/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.52",
|
|
4
|
+
"d": "10/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "validate_page now errors when an element type that the renderer cannot animate (any type other than group, image-block, text-block, rectangle,…",
|
|
7
|
+
"vi": "validate_page nay báo lỗi khi một element có loại không được renderer hỗ trợ animation (chỉ group, image-block, text-block, rectangle, button,…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.0.51",
|
|
4
11
|
"d": "10/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "New patch_page tool edits an existing page by element id without re-sending the whole source: the agent sends per-element ops (update, replace,…",
|
|
35
42
|
"vi": "Công cụ patch_page mới cho phép chỉnh sửa trang hiện có theo element id mà không cần gửi lại toàn bộ source: agent gửi các op theo element (update,…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.46",
|
|
39
|
-
"d": "09/06/2026",
|
|
40
|
-
"type": "Changed",
|
|
41
|
-
"en": "validate_page now emits an advisory warning when no section, button, or text on the page carries a non-neutral color (white, black, or grey),…",
|
|
42
|
-
"vi": "validate_page nay phát cảnh báo tư vấn khi không có section, button hay text nào trên trang mang màu thực sự (không phải trắng, đen hoặc xám), giúp…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -101,7 +101,7 @@ RULES
|
|
|
101
101
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
102
102
|
- Every form input MUST have a unique specials.field_name.
|
|
103
103
|
- events item: { "id", "type", "action", "target", ...action-specific extra fields }. TRIGGER (type): click & hover on any element; success & error on a FORM (success = after a successful submit, error = on validation failure); delay on any element (when it scrolls into view); unset on init. Action vocab per trigger: click→CLICK_ACTIONS, hover→HOVER_ACTIONS, success→SUCCESS_ACTIONS, error→ERROR_ACTIONS, delay→DELAY_ACTIONS (all returned by get_generation_guide). For element-targeting actions (open_popup, close_popup, scroll_to, show_section, hide_section, show_hide_element, change_tab, collapse) target = the target element's id; open_link/download_file target = URL; open_sms/send_email/phone_call target = phone/email; copy target = text (or element id when copyType='elementValue'); set_field_value target = field_name; target may be null (e.g. animation_hover). Each action also reads extra fields (e.g. open_link→targetURL/delayTime, scroll_to→scrollMore, change_tab→moveTo/tabIndex, lightbox→typeLightbox/alt, show_hide_element→onlyMode, open_app→appTarget+provider fields, set_field_value→set_value) — see the action maps for the full list.
|
|
104
|
-
- ANIMATION: each breakpoint's config has config.animation = { "name":"none", "delay":0, "duration":3, "repeat":null }.
|
|
104
|
+
- ANIMATION: each breakpoint's config has config.animation = { "name":"none", "delay":0, "duration":3, "repeat":null }. Animations only run on these 9 element types: group, image-block, text-block, rectangle, button, countdown, line, list-paragraph, notify (renderer contract: landing_page_build/render/build/animate.js). Any other type with a non-"none" name renders stuck/dim in its pre-animation state — keep "none" on all other types. The name must be from the editor's animate.css set; common entrance families: fadeIn* (fadeInUp, fadeInDown, fadeInLeft, fadeInRight…), slideIn* (slideInUp, slideInDown, slideInLeft, slideInRight), zoomIn* (zoomIn, zoomInUp, zoomInDown…), bounceIn* (bounceIn, bounceInUp…), backIn* (backInDown, backInLeft…), flipIn* (flipInX, flipInY), lightSpeedIn* (lightSpeedInLeft, lightSpeedInRight), rotateIn* (rotateIn, rotateInDownLeft…), rollIn, jackInTheBox; attention seekers: bounce, pulse, tada, headShake, wobble, jello, heartBeat, rubberBand, shakeX, shakeY. The full set is enforced by validate_page — use an invalid name and the element renders stuck. NEVER set styles.opacity < 1 for a "subtle" or "muted" look — opacity is permanent and renders the element and all its content faded forever; use rgba() alpha on the color or background property instead.
|
|
105
105
|
- Real data the page DISPLAYS must come from the user — never invent it: phone/hotline/Zalo, price (+ original price), address, shop/brand name, links/URLs, email, opening hours, exact stats/social-proof numbers. If a value the page needs is missing, ASK for it (in intake, or pause before generating); use a clearly-labelled placeholder ONLY when the user explicitly declines, and tell them exactly what to fill. Write ALL page copy in the SAME language the user is chatting in (mirror it), with FULL, CORRECT diacritics/accents — for Vietnamese this means proper dấu (e.g. "Trân Trọng Kính Mời", "Ngày 15 Tháng 08 Năm 2025"), NEVER accent-stripped "không dấu" text. Do not romanize, transliterate, or drop accent marks from any language.
|
|
106
106
|
|
|
107
107
|
INTAKE — act as a DESIGN CONSULTANT, not a form. Goal: understand what the customer actually wants, then design as close to their intent as possible. Ask BEFORE generating, EVERY time (even a "quick"/"test" page).
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import Ajv2020Module from "ajv/dist/2020.js";
|
|
11
11
|
import { CONTAINER_TYPES, FIELD_TYPES } from "./elements/index.js";
|
|
12
|
+
import { ANIMATABLE_TYPES, ANIMATION_NAMES } from "./vocab.js";
|
|
12
13
|
// ajv ships as CJS; under Node16 ESM the constructor is on `.default`.
|
|
13
14
|
const Ajv2020 = Ajv2020Module.default ?? Ajv2020Module;
|
|
14
15
|
// Loaded at runtime (the build copies this JSON beside the compiled validator)
|
|
@@ -210,6 +211,48 @@ export function validatePage(input) {
|
|
|
210
211
|
});
|
|
211
212
|
}
|
|
212
213
|
}
|
|
214
|
+
// animation contract — checked per breakpoint
|
|
215
|
+
// Source: landing_page_build/render/build/animate.js (animatable type list)
|
|
216
|
+
// landing_page_backend/assets/editor/main/traits/TraitAnimation.vue (name set)
|
|
217
|
+
for (const bp of ["desktop", "mobile"]) {
|
|
218
|
+
const anim = node.responsive?.[bp]?.config?.animation;
|
|
219
|
+
if (!anim || typeof anim !== "object")
|
|
220
|
+
continue;
|
|
221
|
+
const animName = anim.name;
|
|
222
|
+
if (typeof animName !== "string" || animName === "none")
|
|
223
|
+
continue;
|
|
224
|
+
// name is present and not 'none' — check type animatability first
|
|
225
|
+
if (type && !ANIMATABLE_TYPES.has(type)) {
|
|
226
|
+
errors.push(`${path} (${type}) [${bp}]: the renderer cannot animate type "${type}" — ` +
|
|
227
|
+
`the element will render stuck/dim in its pre-animation state. ` +
|
|
228
|
+
`Fix: patch_page setting config:{${bp}:{animation:{name:'none',delay:0,duration:3,repeat:null}}} ` +
|
|
229
|
+
`or move the animation onto an animatable wrapper (e.g. type "group").`);
|
|
230
|
+
}
|
|
231
|
+
// name must be in the known animate.css set
|
|
232
|
+
if (!ANIMATION_NAMES.has(animName)) {
|
|
233
|
+
errors.push(`${path} (${type ?? "?"}) [${bp}]: animation name "${animName}" is not in the editor's ` +
|
|
234
|
+
`animate.css set — the keyframe is unknown and the animation never runs. ` +
|
|
235
|
+
`Valid examples: fadeInUp, slideInLeft, zoomIn, bounceIn, backInDown, flipInX, lightSpeedInLeft, rotateIn, rollIn, jackInTheBox.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// styles.opacity < 1 renders the element permanently faded (exportCss.js emits opacity:<v>)
|
|
239
|
+
for (const bp of ["desktop", "mobile"]) {
|
|
240
|
+
const styles = node.responsive?.[bp]?.styles;
|
|
241
|
+
if (!styles || typeof styles !== "object")
|
|
242
|
+
continue;
|
|
243
|
+
const raw = styles.opacity;
|
|
244
|
+
if (raw === undefined || raw === null)
|
|
245
|
+
continue;
|
|
246
|
+
const v = typeof raw === "number" ? raw : typeof raw === "string" ? parseFloat(raw) : NaN;
|
|
247
|
+
if (!Number.isFinite(v))
|
|
248
|
+
continue; // non-numeric garbage → schema territory, skip
|
|
249
|
+
if (v < 1) {
|
|
250
|
+
warnings.push(`${path} (${type ?? "?"}) [${bp}]: styles.opacity=${v} — ` +
|
|
251
|
+
`the element will render permanently faded. ` +
|
|
252
|
+
`If unintended, fix via patch_page({op:'update',id:'${node.id ?? "?"}',styles:{${bp}:{opacity:1}}}); ` +
|
|
253
|
+
`for a muted color use rgba() alpha on the color/background property instead.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
213
256
|
// countdown.language must be a key the renderer's lang table knows (or 'custom');
|
|
214
257
|
// anything else (e.g. a locale code "vi"/"en") crashes the renderer with
|
|
215
258
|
// "is not iterable" when it destructures lang[language].
|
|
@@ -5,6 +5,47 @@
|
|
|
5
5
|
* Derived from assets/render_v4/event/index.js.
|
|
6
6
|
*/
|
|
7
7
|
export const CANVAS = { desktopWidth: 960, mobileWidth: 420, defaultSectionHeight: 800 };
|
|
8
|
+
/**
|
|
9
|
+
* Element types the runtime animator handles.
|
|
10
|
+
* Source: landing_page_build/render/build/animate.js — the switch statement
|
|
11
|
+
* that emits the animation CSS class only covers these 9 types. Any other type
|
|
12
|
+
* with config.animation.name != 'none' produces a broken CSS selector and the
|
|
13
|
+
* element stays in the pre-animation (dim/hidden) state permanently.
|
|
14
|
+
*/
|
|
15
|
+
export const ANIMATABLE_TYPES = new Set([
|
|
16
|
+
"group", "image-block", "text-block", "rectangle", "button",
|
|
17
|
+
"countdown", "line", "list-paragraph", "notify",
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Valid animation name values accepted by the editor and the renderer.
|
|
21
|
+
* Source: landing_page_backend/assets/editor/main/traits/TraitAnimation.vue
|
|
22
|
+
* (the animate.css-backed option list). Any name outside this set produces
|
|
23
|
+
* an unknown keyframe — the animation never runs and the element may render stuck.
|
|
24
|
+
*/
|
|
25
|
+
export const ANIMATION_NAMES = new Set([
|
|
26
|
+
"none",
|
|
27
|
+
"bounce", "flash", "pulse", "rubberBand", "shakeX", "shakeY", "headShake",
|
|
28
|
+
"swing", "swingCenter", "tada", "wobble", "jello", "heartBeat",
|
|
29
|
+
"backInDown", "backInLeft", "backInRight", "backInUp",
|
|
30
|
+
"backOutDown", "backOutLeft", "backOutRight", "backOutUp",
|
|
31
|
+
"bounceIn", "bounceInDown", "bounceInLeft", "bounceInRight", "bounceInUp",
|
|
32
|
+
"bounceOut", "bounceOutDown", "bounceOutLeft", "bounceOutRight", "bounceOutUp",
|
|
33
|
+
"fadeIn", "fadeInDown", "fadeInDownBig", "fadeInLeft", "fadeInLeftBig",
|
|
34
|
+
"fadeInRight", "fadeInRightBig", "fadeInUp", "fadeInUpBig",
|
|
35
|
+
"fadeInTopLeft", "fadeInTopRight", "fadeInBottomLeft", "fadeInBottomRight",
|
|
36
|
+
"fadeOut", "fadeOutDown", "fadeOutDownBig", "fadeOutLeft", "fadeOutLeftBig",
|
|
37
|
+
"fadeOutRight", "fadeOutRightBig", "fadeOutUp", "fadeOutUpBig",
|
|
38
|
+
"fadeOutTopLeft", "fadeOutTopRight", "fadeOutBottomRight", "fadeOutBottomLeft",
|
|
39
|
+
"flip", "flipInX", "flipInY", "flipOutX", "flipOutY",
|
|
40
|
+
"lightSpeedInRight", "lightSpeedInLeft", "lightSpeedOutRight", "lightSpeedOutLeft",
|
|
41
|
+
"rotateIn", "rotateInDownLeft", "rotateInDownRight", "rotateInUpLeft", "rotateInUpRight",
|
|
42
|
+
"rotateOut", "rotateOutDownLeft", "rotateOutDownRight", "rotateOutUpLeft", "rotateOutUpRight",
|
|
43
|
+
"hinge", "jackInTheBox", "rollIn", "rollOut",
|
|
44
|
+
"zoomIn", "zoomInDown", "zoomInLeft", "zoomInRight", "zoomInUp",
|
|
45
|
+
"zoomOut", "zoomOutDown", "zoomOutLeft", "zoomOutRight", "zoomOutUp",
|
|
46
|
+
"slideInDown", "slideInLeft", "slideInRight", "slideInUp",
|
|
47
|
+
"slideOutDown", "slideOutLeft", "slideOutRight", "slideOutUp",
|
|
48
|
+
]);
|
|
8
49
|
export const EVENT_TRIGGERS = ["click", "hover", "success", "error", "unset", "delay"];
|
|
9
50
|
// Click-trigger actions. "Extra:" lists the action-specific event-object fields
|
|
10
51
|
// the dispatcher reads beyond { id, type, action, target } (render_v4/event/index.js).
|
package/dist/smoke.js
CHANGED
|
@@ -494,5 +494,116 @@ console.log("== draft-cache: update draft round-trip (update_page / live-page pa
|
|
|
494
494
|
deleteDraft(uid);
|
|
495
495
|
check("update draft: deleteDraft removes entry", getDraft(uid) === null, getDraft(uid));
|
|
496
496
|
}
|
|
497
|
+
console.log("== validate: animation contract checks ==");
|
|
498
|
+
{
|
|
499
|
+
const mkSec = (children) => ({
|
|
500
|
+
id: "anim_sec",
|
|
501
|
+
type: "section",
|
|
502
|
+
properties: { name: "S", movable: false, sync: true },
|
|
503
|
+
responsive: {
|
|
504
|
+
desktop: { config: {}, styles: { position: "relative", height: 800, background: "rgba(255,255,255,1)" } },
|
|
505
|
+
mobile: { config: {}, styles: { position: "relative", height: 800, background: "rgba(255,255,255,1)" } },
|
|
506
|
+
},
|
|
507
|
+
specials: {}, runtime: {}, events: [],
|
|
508
|
+
children,
|
|
509
|
+
});
|
|
510
|
+
const mkEl = (id, type, animName, bp = "both") => ({
|
|
511
|
+
id,
|
|
512
|
+
type,
|
|
513
|
+
properties: { name: "el", movable: true, sync: true },
|
|
514
|
+
responsive: {
|
|
515
|
+
desktop: {
|
|
516
|
+
config: bp === "mobile" ? {} : { animation: { name: animName, delay: 0, duration: 3, repeat: null } },
|
|
517
|
+
styles: { top: 10, left: 10, width: 100, height: 40 },
|
|
518
|
+
},
|
|
519
|
+
mobile: {
|
|
520
|
+
config: bp === "desktop" ? {} : { animation: { name: animName, delay: 0, duration: 3, repeat: null } },
|
|
521
|
+
styles: { top: 10, left: 10, width: 100, height: 40 },
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
specials: type === "button" ? { text: "X" } : type === "text-block" ? { text: "X" } : {},
|
|
525
|
+
runtime: {}, events: [],
|
|
526
|
+
});
|
|
527
|
+
// 1) non-animatable type (form) with a real animation name → ERROR
|
|
528
|
+
const rForm = validatePage({
|
|
529
|
+
page: [mkSec([mkEl("f1", "form", "fadeInUp")])],
|
|
530
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
531
|
+
});
|
|
532
|
+
check("anim: non-animatable type (form) fadeInUp → error", rForm.errors.some((e) => e.includes("cannot animate") && e.includes("form")), rForm.errors);
|
|
533
|
+
// 2) bogus animation name on a text-block → ERROR
|
|
534
|
+
const rBogus = validatePage({
|
|
535
|
+
page: [mkSec([mkEl("t1", "text-block", "fade-in-up")])],
|
|
536
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
537
|
+
});
|
|
538
|
+
check("anim: bogus name 'fade-in-up' on text-block → error", rBogus.errors.some((e) => e.includes('"fade-in-up"') && e.includes("not in the editor")), rBogus.errors);
|
|
539
|
+
// 3) non-animatable type (html-box) with bogus name → BOTH errors
|
|
540
|
+
const rHtmlBad = validatePage({
|
|
541
|
+
page: [mkSec([mkEl("h1", "html-box", "fade-in-up")])],
|
|
542
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
543
|
+
});
|
|
544
|
+
check("anim: html-box + bogus name → type error present", rHtmlBad.errors.some((e) => e.includes("cannot animate") && e.includes("html-box")), rHtmlBad.errors);
|
|
545
|
+
check("anim: html-box + bogus name → name error present", rHtmlBad.errors.some((e) => e.includes('"fade-in-up"')), rHtmlBad.errors);
|
|
546
|
+
// 4) valid name on animatable type → NO animation error
|
|
547
|
+
const rGood = validatePage({
|
|
548
|
+
page: [mkSec([mkEl("t2", "text-block", "fadeInUp")])],
|
|
549
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
550
|
+
});
|
|
551
|
+
check("anim: valid 'fadeInUp' on text-block → no animation error", !rGood.errors.some((e) => e.includes("animate") || e.includes("keyframe")), rGood.errors);
|
|
552
|
+
// 5) name 'none' on any type → no error at all
|
|
553
|
+
const rNone = validatePage({
|
|
554
|
+
page: [mkSec([mkEl("f2", "form", "none")])],
|
|
555
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
556
|
+
});
|
|
557
|
+
check("anim: name='none' on form → no animation error", !rNone.errors.some((e) => e.includes("animate")), rNone.errors);
|
|
558
|
+
// 6) absent animation object → no error
|
|
559
|
+
const rNoAnim = validatePage({
|
|
560
|
+
page: [mkSec([{
|
|
561
|
+
id: "f3", type: "form",
|
|
562
|
+
properties: { name: "el", movable: true, sync: true },
|
|
563
|
+
responsive: {
|
|
564
|
+
desktop: { config: {}, styles: { top: 10, left: 10, width: 100, height: 40 } },
|
|
565
|
+
mobile: { config: {}, styles: { top: 10, left: 10, width: 100, height: 40 } },
|
|
566
|
+
},
|
|
567
|
+
specials: {}, runtime: {}, events: [], children: [],
|
|
568
|
+
}])],
|
|
569
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
570
|
+
});
|
|
571
|
+
check("anim: absent animation config on form → no animation error", !rNoAnim.errors.some((e) => e.includes("animate")), rNoAnim.errors);
|
|
572
|
+
// 7) styles.opacity 0.4 (number) → WARNING
|
|
573
|
+
const mkOpacity = (id, type, opacity) => ({
|
|
574
|
+
id,
|
|
575
|
+
type,
|
|
576
|
+
properties: { name: "el", movable: true, sync: true },
|
|
577
|
+
responsive: {
|
|
578
|
+
desktop: { config: {}, styles: { top: 10, left: 10, width: 100, height: 40, opacity } },
|
|
579
|
+
mobile: { config: {}, styles: { top: 10, left: 10, width: 100, height: 40 } },
|
|
580
|
+
},
|
|
581
|
+
specials: type === "button" ? { text: "X" } : {},
|
|
582
|
+
runtime: {}, events: [],
|
|
583
|
+
});
|
|
584
|
+
const rOp04 = validatePage({
|
|
585
|
+
page: [mkSec([mkOpacity("b1", "button", 0.4)])],
|
|
586
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
587
|
+
});
|
|
588
|
+
check("opacity: 0.4 number → warning present", rOp04.warnings.some((w) => w.includes("opacity=0.4") && w.includes("permanently faded")), rOp04.warnings);
|
|
589
|
+
// 8) styles.opacity "0.4" (numeric string) → WARNING
|
|
590
|
+
const rOpStr = validatePage({
|
|
591
|
+
page: [mkSec([mkOpacity("b2", "button", "0.4")])],
|
|
592
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
593
|
+
});
|
|
594
|
+
check("opacity: '0.4' string → warning present", rOpStr.warnings.some((w) => w.includes("opacity=0.4") && w.includes("permanently faded")), rOpStr.warnings);
|
|
595
|
+
// 9) styles.opacity 1 → NO warning
|
|
596
|
+
const rOp1 = validatePage({
|
|
597
|
+
page: [mkSec([mkOpacity("b3", "button", 1)])],
|
|
598
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
599
|
+
});
|
|
600
|
+
check("opacity: 1 → no opacity warning", !rOp1.warnings.some((w) => w.includes("permanently faded")), rOp1.warnings);
|
|
601
|
+
// 10) non-numeric opacity (e.g. "inherit") → NO warning (schema territory)
|
|
602
|
+
const rOpStr2 = validatePage({
|
|
603
|
+
page: [mkSec([mkOpacity("b4", "button", "inherit")])],
|
|
604
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
605
|
+
});
|
|
606
|
+
check("opacity: 'inherit' string → no opacity warning", !rOpStr2.warnings.some((w) => w.includes("permanently faded")), rOpStr2.warnings);
|
|
607
|
+
}
|
|
497
608
|
console.log(`\n${failures === 0 ? "ALL GOOD" : failures + " FAILURE(S)"}`);
|
|
498
609
|
process.exit(failures === 0 ? 0 : 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-landing-mcp",
|
|
6
6
|
"type": "module",
|