webcake-landing-mcp 1.0.51 → 1.0.53

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.
@@ -35,10 +35,22 @@ function parseArgs(argv) {
35
35
  }
36
36
  function openBrowser(url) {
37
37
  const platform = process.platform;
38
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
39
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
40
38
  try {
41
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
39
+ if (platform === "win32") {
40
+ // `cmd /c start` parses an unquoted `&` as a command separator, which cuts
41
+ // the connect URL right before `&state=...` (the login then bounces back to
42
+ // the loopback without state and is rejected). Pass the args verbatim with
43
+ // the URL double-quoted so cmd hands `start` the full URL. The first quoted
44
+ // arg ("") is `start`'s window title.
45
+ spawn("cmd", ["/c", "start", '""', `"${url}"`], {
46
+ stdio: "ignore",
47
+ detached: true,
48
+ windowsVerbatimArguments: true,
49
+ }).unref();
50
+ return;
51
+ }
52
+ const cmd = platform === "darwin" ? "open" : "xdg-open";
53
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
42
54
  }
43
55
  catch {
44
56
  /* ignore — the URL is also printed */
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.0.53",
4
+ "d": "10/06/2026",
5
+ "type": "Fixed",
6
+ "en": "The login command on Windows now opens the connect URL correctly: cmd /c start previously split the URL at the first & character (treating it as a…",
7
+ "vi": "Lệnh login trên Windows nay mở URL kết nối chính xác: trước đây cmd /c start tách URL tại ký tự & đầu tiên (do hiểu & là dấu phân cách lệnh), khiến…"
8
+ },
9
+ {
10
+ "v": "1.0.52",
11
+ "d": "10/06/2026",
12
+ "type": "Added",
13
+ "en": "validate_page now errors when an element type that the renderer cannot animate (any type other than group, image-block, text-block, rectangle,…",
14
+ "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,…"
15
+ },
2
16
  {
3
17
  "v": "1.0.51",
4
18
  "d": "10/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Added",
27
41
  "en": "create_page now caches the expanded source in an in-memory draft store when validation fails and returns a draft_id alongside the validation errors,…",
28
42
  "vi": "create_page nay lưu source đã expand vào bộ nhớ draft khi xác thực thất bại và trả về draft_id kèm theo danh sách lỗi, cho phép agent chỉ sửa các…"
29
- },
30
- {
31
- "v": "1.0.47",
32
- "d": "10/06/2026",
33
- "type": "Added",
34
- "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
- "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 }. Keep "none" unless an entrance animation is wanted.
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.51",
3
+ "version": "1.0.53",
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",