webcake-landing-mcp 1.0.73 → 1.0.74
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 +7 -7
- package/dist/domains/landing/guide.js +1 -0
- package/dist/domains/landing/instructions.js +2 -1
- package/dist/domains/landing/page-schema.json +5 -3
- package/dist/domains/landing/validate.js +19 -0
- package/dist/persistence/html-ingest.js +11 -3
- package/dist/persistence/ingest/semantic.js +47 -2
- package/dist/persistence/ingest/tailwind.js +43 -0
- package/dist/smoke.js +57 -0
- package/package.json +1 -1
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.74",
|
|
4
|
+
"d": "13/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "ingest_html and ingest_url now extract Tailwind gradient utilities (bg-gradient-to-*/from-*/via-*/to-*) from the page's class attributes, resolve…",
|
|
7
|
+
"vi": "ingest_html và ingest_url nay trích xuất các utility gradient Tailwind (bg-gradient-to-*/from-*/via-*/to-*) từ các thuộc tính class của trang,…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.0.73",
|
|
4
11
|
"d": "13/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Changed",
|
|
34
41
|
"en": "upload_images dry-run response now returns an action_required field (replacing the previous soft hint) that explicitly blocks the model from…",
|
|
35
42
|
"vi": "Phản hồi dry-run của upload_images nay trả về trường action_required (thay thế hint mềm trước đó) để chặn model lắp ráp trang hoặc dùng placeholder…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.68",
|
|
39
|
-
"d": "12/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "ingest_html and ingest_url now auto-detect absolute-canvas builder exports (LadiPage-family pages and Webcake-published HTML): bare positioned-div…",
|
|
42
|
-
"vi": "ingest_html và ingest_url nay tự động phát hiện các bản export từ builder absolute-canvas (trang LadiPage-family và HTML đã publish của Webcake):…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -120,6 +120,7 @@ RULES
|
|
|
120
120
|
- Every form input MUST have a unique specials.field_name.
|
|
121
121
|
- 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.
|
|
122
122
|
- 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.
|
|
123
|
+
- BEYOND ELEMENT CAPABILITY (custom CSS / class / JS — use these so the page is as COMPLETE as possible instead of dropping an effect the built-in specials can't express): the renderer supports real escape hatches — REACH FOR THEM when a reference (esp. a Google Stitch / Tailwind page) has hover/transition effects, gradients, glassmorphism, custom shadows, gradient/clipped text, sticky tricks, or keyframe animations outside the 9-type entrance set. Tools, in order of preference: (1) element specials.custom_css — extra CSS DECLARATIONS injected INTO that element's own rule "#w-<id>{ … }" (declarations only — NO selector, NO :hover, NO @keyframes). Requires specials.customAdvance:true (without it the renderer drops it). Use for: "background:linear-gradient(...)" (gradient button/hero — also reproduces the AST's gradients), "box-shadow:0 20px 40px rgba(0,0,0,.08)", "backdrop-filter:blur(20px)" (glass header — pair with an rgba background), "transition:transform .3s ease", "-webkit-background-clip:text;-webkit-text-fill-color:transparent" (gradient text). (2) element specials.custom_class (comma-separated, also needs customAdvance:true) — adds class names to "#w-<id>" so settings.extra_css can target a group of elements. (3) page settings.extra_css — a FULL stylesheet injected raw into <head>: this is where SELECTORS live — :hover, :focus, @keyframes, media queries. The DOM id of any element is "#w-<its id>". THIS is how you implement the AST's per-section hover_effects: scale → "#w-<id>{transition:transform .3s} #w-<id>:hover{transform:scale(1.05)}"; card lift → "#w-<id>:hover{transform:translateY(-8px)}"; image-zoom in a card → "#w-<cardId>{overflow:hidden} #w-<imgId>{transition:transform .5s} #w-<cardId>:hover #w-<imgId>{transform:scale(1.1)}"; color/underline on hover are also fine as a hover EVENT (change_color / change_underline) — pick whichever, but DON'T silently drop the effect. (4) page settings.extra_script — raw JS injected before </body> for behavior the event vocab can't express. (5) page settings.bhet ('before </head>') and settings.bbet ('before </body>') — raw HTML BLOCKS (not just CSS/JS): bhet for <link> webfonts/stylesheets, <style>, <meta>, verification tags, analytics/pixels (GTM, FB); bbet for chat widgets, deferred <script>, GTM <noscript>, third-party embeds. Use these when you need a real font file, a tag/pixel, or an external widget — extra_css/extra_script are for raw CSS/JS only, bhet/bbet take arbitrary HTML (valid markup only). RULES: custom_css is declarations-only (validate_page warns if you put a selector there) and needs customAdvance:true (validate_page warns if missing); extra_css/extra_script are raw and unscoped, so keep selectors specific to "#w-<id>" / your custom_class to avoid bleeding into the rest of the page; don't use these to replace a capability a built-in special already has (e.g. entrance animation → config.animation; click behavior → events).
|
|
123
124
|
- 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.
|
|
124
125
|
|
|
125
126
|
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).
|
|
@@ -25,7 +25,7 @@ RULES (follow for every request):
|
|
|
25
25
|
A wrong element type is the most common error → { op:'update', id:'<element id>', type:'<allowed type>' } (run list_elements if unsure). Drafts expire in ~2 h.
|
|
26
26
|
- DRY-RUN CACHE: create_page, update_page, and add_section dry_run=true all cache the validated payload and return a draft_id. Re-run the same tool with { draft_id, dry_run:false } — no need to re-send the source.
|
|
27
27
|
- Organizations: call list_organizations before saving. If the account has exactly ONE org, create_page auto-selects it — no need to ask. If there are MULTIPLE orgs, show them and ask the user which to use (is_default is the suggested default); pass the chosen organization_id to create_page. Pass organization_id:"personal" only when the user explicitly wants no org. create_page enforces this itself (it refuses to guess between multiple orgs). Endpoints are owner-scoped (only the account's own pages).
|
|
28
|
-
- REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements using role→element hints (hero → section with background image/overlay + heading + paragraph + button; features → group blocks with icon/title/text; stats bar → group with heading+text per stat; pricing → group with text list + button; footer → section with text + links). When the reference contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box (clone its HTML with all styles inlined), not as element soup. (2) HTML string → call ingest_html(html, detail:'full') to get the richer AST when cloning; use detail:'compact' (default) for a layout-only reference. (3) URL → call ingest_url(url, detail:'full') for the same richer AST. (4) GOOGLE STITCH design (when a Stitch MCP is connected and the user points at a Stitch screen/project) → bridge the two MCPs: call the Stitch tool get_screen, take the returned htmlCode.downloadUrl, and pass THAT url to ingest_url(detail:'full') (or fetch the HTML and use ingest_html) — Stitch output is a Tailwind-CDN page, so the AST comes back with a token-named palette + design_tokens (the real spacing/radii/type-scale); LOCK the design system from those tokens, reuse the page's image URLs (they auto-host on save — Stitch's googleusercontent images included), then create_page. ABSOLUTE-CANVAS builder exports (LadiPage-family / Webcake-published HTML) are AUTO-DETECTED by ingest_html/ingest_url and converted DETERMINISTICALLY into a ready-to-save source (folded into the response as source + clone_notes + clone_notice, the per-element geometry summarized to canvas_summary) — a faithful 1:1 clone on the matching 420/960 canvas: pass that source STRAIGHT to create_page (dry_run:true first to validate, then dry_run:false) instead of hand-rebuilding; its images auto-host on save (no upload_images), and clone_notes lists the few lossy approximations (fixed/floating elements, svg-less shapes, skipped social-proof toasts) to patch_page afterward. The AST classifies sections by role and lists headings/subheadings/ctas/images/form_fields plus brand hints (colors/fonts), a token-named palette + design_tokens (for Tailwind-CDN pages like Google Stitch, lifted from the page's tailwind.config: palette names every color token e.g. primary→#a43b38, and design_tokens carries the resolved spacing grid + corner radii + TYPE SCALE e.g. display-lg→48px — rebuild sizing/spacing/color FROM these tokens, not by guessing), background_images from stylesheets, and in full mode: per-section blocks (cards/tiles/steps with title/body/image/cta)
|
|
28
|
+
- REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements using role→element hints (hero → section with background image/overlay + heading + paragraph + button; features → group blocks with icon/title/text; stats bar → group with heading+text per stat; pricing → group with text list + button; footer → section with text + links). When the reference contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box (clone its HTML with all styles inlined), not as element soup. (2) HTML string → call ingest_html(html, detail:'full') to get the richer AST when cloning; use detail:'compact' (default) for a layout-only reference. (3) URL → call ingest_url(url, detail:'full') for the same richer AST. (4) GOOGLE STITCH design (when a Stitch MCP is connected and the user points at a Stitch screen/project) → bridge the two MCPs: call the Stitch tool get_screen, take the returned htmlCode.downloadUrl, and pass THAT url to ingest_url(detail:'full') (or fetch the HTML and use ingest_html) — Stitch output is a Tailwind-CDN page, so the AST comes back with a token-named palette + design_tokens (the real spacing/radii/type-scale) + gradients (Tailwind gradient utilities reconstructed as linear-gradient(...) with resolved color stops — apply them as the element's background, e.g. on CTA/hero) + per-section hover_effects (the interactions Stitch packs in: scale/image-zoom/lift/underline/color-change — REPRODUCE them via Webcake hover events: change_color (change_color_type text/background/border) for color/underline, the button's hovered* specials or animation_hover for scale/lift/zoom — don't ship a static page); LOCK the design system from those tokens, reuse the page's image URLs (they auto-host on save — Stitch's googleusercontent images included), then create_page. ABSOLUTE-CANVAS builder exports (LadiPage-family / Webcake-published HTML) are AUTO-DETECTED by ingest_html/ingest_url and converted DETERMINISTICALLY into a ready-to-save source (folded into the response as source + clone_notes + clone_notice, the per-element geometry summarized to canvas_summary) — a faithful 1:1 clone on the matching 420/960 canvas: pass that source STRAIGHT to create_page (dry_run:true first to validate, then dry_run:false) instead of hand-rebuilding; its images auto-host on save (no upload_images), and clone_notes lists the few lossy approximations (fixed/floating elements, svg-less shapes, skipped social-proof toasts) to patch_page afterward. The AST classifies sections by role and lists headings/subheadings/ctas/images/form_fields plus brand hints (colors/fonts), a token-named palette + design_tokens (for Tailwind-CDN pages like Google Stitch, lifted from the page's tailwind.config: palette names every color token e.g. primary→#a43b38, and design_tokens carries the resolved spacing grid + corner radii + TYPE SCALE e.g. display-lg→48px — rebuild sizing/spacing/color FROM these tokens, not by guessing), background_images from stylesheets, gradients (incl. reconstructed Tailwind gradient backgrounds), per-section hover_effects (reproduce them — via hover events, or the custom-CSS escape hatch below), and in full mode: per-section blocks (cards/tiles/steps with title/body/image/cta) and li lists — use it for LAYOUT + HIERARCHY, then generate FRESH content tailored to the user's brand (don't 1:1 copy text). IMAGES FROM THE REFERENCE ARE THE USER'S ASSETS — for BOTH intents (adapt AND clone), put every real image URL found in the AST (images, background_images, og_image, canvas src/background) straight into the corresponding slot's specials.src / url(...) background / gallery link; the save AUTO-HOSTS it to the Webcake CDN, so you do NOT need to call upload_images first — just carry the URL through un-altered (never drop it, never swap it for a search_images stock photo or a placeholder). Call search_images ONLY for slots that have no source image in the reference. intent='clone' only when the user explicitly asks to mirror the original; default intent='adapt' (adapt rewrites TEXT, not the imagery). The reference workflow PRESERVES craft rules above (centering, page margin, premium spacing, real images) — apply them on top of the reference layout, don't bypass them.
|
|
29
29
|
|
|
30
30
|
MODEL (essentials):
|
|
31
31
|
- Top-level: { page:[sections], popup:[popups], dynamic_pages:[], settings:{}, options:{mobileOnly,versionID}, cartConfigs:{isActive:false}, svariations:[] }. Popups are a SEPARATE top-level array, NOT inside page; currency lives in settings.currency (not options). Leave dynamic_pages/svariations as [] for a static page, but keep them on edit round-trips.
|
|
@@ -39,6 +39,7 @@ MODEL (essentials):
|
|
|
39
39
|
- Visible content lives in specials (text, src, field_name…), never in styles. Colors as rgba(). Animation in config.animation={name,delay,duration,repeat}. Form inputs need a unique specials.field_name (use canonical keys: full_name, phone_number, email, address, quantity).
|
|
40
40
|
- IMAGES: include them (hero/product, feature icons, about photo). The server AUTO-HOSTS external image URLs on every save: any real http(s) image URL you put in specials.src / a url(...) background / gallery item.link / video poster is downloaded and re-hosted to the Webcake CDN automatically by create_page/update_page/add_section/patch_page (the result's rehost field reports how many) — so you do NOT have to pre-call upload_images for reference or web images. SOURCE PRIORITY: (1) images the user supplied or that exist in the reference HTML/URL → put the REAL source URL straight into specials.src and let the save host it (use those exact images, never swap them for stock photos; the original URL must reach the save un-altered — NEVER replace a real source image URL with a placeholder). The ONE case you MUST still call upload_images yourself: LOCAL FILE PATHS from the user's computer (pass the path directly in upload_images urls — the save can't read local files; NEVER upload a user's local file to a third-party host like catbox or imgur first), then use the returned URL. (2) only for slots with NO source image → call search_images with a short English subject (e.g. 'fresh coffee cup') and put a returned URL (src.large for a hero/banner, src.medium for a card/thumb) into the image-block specials.src; it works out of the box (a shared proxy supplies images); (3) if search_images returns ok:false / is unreachable / has no fitting photo → find a real image YOURSELF using whatever web search/fetch capability you have (brand site, product page, free-to-use source) and re-host it via upload_images; (4) a PLACEHOLDER sized to the box (https://placehold.co/<width>x<height>) is the LAST resort, only after (2) AND (3) both failed. (gallery.media = array of OBJECTS {type:'image',link:'<url>',linkVideo:'',typeVideo:'youtube',imageCompression:true} — NOT plain strings, the gallery reads item.link; video.specials.img = poster). NEVER leave src empty (renders blank). Ensure text contrasts with its section background.
|
|
41
41
|
|
|
42
|
+
- BEYOND ELEMENT CAPABILITY: when a reference effect can't be expressed with an element's built-in specials (hover scale/lift/zoom & transitions, gradients, glassmorphism/backdrop-blur, custom shadows, gradient/clipped text, keyframe animations outside the 9 entrance types) DON'T drop it — use the escape hatches so the page is as complete as possible: element specials.custom_css (extra DECLARATIONS in #w-<id>{…}; declarations-only) + specials.custom_class (both need specials.customAdvance:true) for per-element styling, and page settings.extra_css (a full raw stylesheet in <head> — where :hover/@keyframes/media-queries live, target #w-<element id>) + settings.extra_script (raw JS) + settings.bhet/bbet (raw HTML blocks at end of <head>/<body> — for webfont <link>s, <meta>/verification, analytics/pixels, chat widgets, third-party embeds). get_generation_guide has the recipes; validate_page warns if custom_css lacks customAdvance or holds a selector.
|
|
42
43
|
- PREVIEW vs PUBLISH: for review share the EDITOR url (the builder renders the raw source). The editor_url SIGNS THE BROWSER IN automatically (it routes through the builder's /transport with the account token), so it works even when the user isn't logged in — but for the same reason it must go to the PAGE OWNER ONLY, never into anything public. create_page AUTO-PUBLISHES on success (builds the rendered app + publish_html), so a fresh page's preview_url renders right away — but the preview host (preview.localhost:5800 local / staging.webcake.me staging / www.webcake.me prod) only serves it for ~10 MINUTES after the last publish, then shows "Preview page is expired". The EDIT routes (update_page/add_section/patch_page) store source only — after finishing a round of edits, run publish_page({ page_id, dry_run:false }) to rebuild the rendered app (else the preview shows the STALE pre-edit build). ONLY a custom_domain (publish_page({ page_id, custom_domain, dry_run:false })) gives a permanent public URL; without one the page has just the ephemeral preview link — say so and suggest attaching a domain the user already points at Webcake.
|
|
43
44
|
|
|
44
45
|
Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, search_images, upload_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
|
|
@@ -58,8 +58,10 @@
|
|
|
58
58
|
"fb_tracking_code": { "type": "string", "description": "Facebook pixel id." },
|
|
59
59
|
"tiktok_script": { "type": "string" },
|
|
60
60
|
"global_track_ids": { "type": "array" },
|
|
61
|
-
"extra_css": { "type": "string", "description": "
|
|
62
|
-
"extra_script": { "type": "string", "description": "
|
|
61
|
+
"extra_css": { "type": "string", "description": "Page-level custom CSS — a FULL stylesheet injected raw into <head>. The escape hatch for effects beyond element specials: SELECTORS, :hover/:focus, @keyframes, media queries. Target any element by its DOM id #w-<element id> (or a specials.custom_class). E.g. hover scale: '#w-abc{transition:transform .3s}#w-abc:hover{transform:scale(1.05)}'. Keep selectors specific (it is raw/unscoped) to avoid bleeding." },
|
|
62
|
+
"extra_script": { "type": "string", "description": "Page-level custom JavaScript injected raw before </body> — behavior beyond the event vocab." },
|
|
63
|
+
"bhet": { "type": "string", "description": "'Before </head>' — a raw HTML block injected at the END of <head>. Unlike extra_css (CSS-only)/extra_script (JS-only) this takes ARBITRARY HTML: <link> webfonts/stylesheets, <style>, <meta>, verification tags, analytics/pixels (GTM, FB). Rendered raw — valid HTML only." },
|
|
64
|
+
"bbet": { "type": "string", "description": "'Before </body>' — a raw HTML block injected at the END of <body>: chat widgets, deferred <script>, GTM <noscript>, third-party embeds. Rendered raw — valid HTML only." },
|
|
63
65
|
"auto_save_draft": { "type": "boolean" },
|
|
64
66
|
"auto_save_info_user": { "type": "boolean" },
|
|
65
67
|
"send_info_to_thank_page": { "type": "boolean" }
|
|
@@ -230,7 +232,7 @@
|
|
|
230
232
|
},
|
|
231
233
|
"specials": {
|
|
232
234
|
"type": "object",
|
|
233
|
-
"description": "Type-specific content/config. The user-visible CONTENT lives here, NOT in styles. Open object; key fields by type: section{globalSection,globalSectionName,custom_class,imageCompression}; text-block{text(HTML),tag}; list-paragraph{text(<li>..)}; image-block{src,resize}; button{text,required,format,connectedSurvey}; video{typeVideo,video_cdn,img,autoReplay}; gallery{media[]}; form{field_type,form_type,sheetOrder,validate,submit_success,fb_event_type,fb_conversion_value,fb_tracking_currency,tiktok_conversion_value,tiktok_tracking_currency}; input/textarea/select/checkbox/radio/address/...{field_name,field_placeholder,field_type,required,options}; countdown{type,duration,startTime,endTime,showDay,showSecond,showText,language(vietnam|english|filipino|khmer|lao|indonesian|thai|malay|custom — NOT a locale code; 'custom' uses customTranslation),customTranslation{day,hour,minute,second}}; survey{options[{id,image,title,value,field_name}],type,multiOption,selectedBackground,selectedBorder}; list-product{format_title,numerical_order,remain_quantity_text}.",
|
|
235
|
+
"description": "Type-specific content/config. The user-visible CONTENT lives here, NOT in styles. Open object; key fields by type: section{globalSection,globalSectionName,custom_class,imageCompression}; text-block{text(HTML),tag}; list-paragraph{text(<li>..)}; image-block{src,resize}; button{text,required,format,connectedSurvey}; video{typeVideo,video_cdn,img,autoReplay}; gallery{media[]}; form{field_type,form_type,sheetOrder,validate,submit_success,fb_event_type,fb_conversion_value,fb_tracking_currency,tiktok_conversion_value,tiktok_tracking_currency}; input/textarea/select/checkbox/radio/address/...{field_name,field_placeholder,field_type,required,options}; countdown{type,duration,startTime,endTime,showDay,showSecond,showText,language(vietnam|english|filipino|khmer|lao|indonesian|thai|malay|custom — NOT a locale code; 'custom' uses customTranslation),customTranslation{day,hour,minute,second}}; survey{options[{id,image,title,value,field_name}],type,multiOption,selectedBackground,selectedBorder}; list-product{format_title,numerical_order,remain_quantity_text}. UNIVERSAL escape-hatch keys (any element): customAdvance(boolean — MUST be true for the next two to apply, else the renderer drops them), custom_css(string — extra CSS DECLARATIONS injected inside this element's #w-<id>{…} rule; declarations only, no selector/:hover/@keyframes — those go in settings.extra_css), custom_class(string — comma-separated extra class names added to #w-<id>, to target from settings.extra_css). Section-only: isCustomTracking(boolean)+customTracking(string HTML/JS) for per-section tracking snippets.",
|
|
234
236
|
"additionalProperties": true,
|
|
235
237
|
"properties": {
|
|
236
238
|
"text": { "type": "string", "description": "text-block/button label/list-paragraph (may contain HTML)." },
|
|
@@ -298,6 +298,25 @@ export function validatePage(input) {
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
// custom CSS/class escape hatches (effects beyond an element's built-in specials).
|
|
302
|
+
// Renderer gates BOTH on specials.customAdvance===true (render/build/index.js
|
|
303
|
+
// custom_class + exportCss.js custom_css); without it they are silently dropped.
|
|
304
|
+
// And custom_css is injected as plain DECLARATIONS inside #w-<id>{…}, so a
|
|
305
|
+
// selector / :hover / @keyframes there corrupts the rule — those go in
|
|
306
|
+
// settings.extra_css (full stylesheet, injected raw into <head>).
|
|
307
|
+
{
|
|
308
|
+
const sp = node.specials;
|
|
309
|
+
if (sp && typeof sp === "object") {
|
|
310
|
+
const hasCustom = (typeof sp.custom_css === "string" && sp.custom_css.trim() !== "") ||
|
|
311
|
+
(typeof sp.custom_class === "string" && sp.custom_class.trim() !== "");
|
|
312
|
+
if (hasCustom && sp.customAdvance !== true) {
|
|
313
|
+
warnings.push(`${path} (${type}): specials.custom_css/custom_class is set but specials.customAdvance!==true — the renderer ignores both. Set "customAdvance": true.`);
|
|
314
|
+
}
|
|
315
|
+
if (typeof sp.custom_css === "string" && /[{}]|@keyframes|:hover|:focus|::/.test(sp.custom_css)) {
|
|
316
|
+
warnings.push(`${path} (${type}): specials.custom_css is injected as plain declarations inside #w-${node.id}{…} — a selector/:hover/@keyframes/media-query there breaks the rule. Keep declarations only here (e.g. "box-shadow:0 20px 40px rgba(0,0,0,.08);backdrop-filter:blur(20px);"); put hover/keyframes/media rules in settings.extra_css targeting #w-${node.id} (or a specials.custom_class).`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
301
320
|
// animation contract — checked per breakpoint
|
|
302
321
|
// Source: landing_page_build/render/build/animate.js (animatable type list)
|
|
303
322
|
// landing_page_backend/assets/editor/main/traits/TraitAnimation.vue (name set)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { parse } from "node-html-parser";
|
|
17
17
|
import { extractStyleBlocks, extractGoogleFonts, extractGradients, fixMojibake } from "./ingest/stylesheets.js";
|
|
18
18
|
import { extractTailwindConfig } from "./ingest/tailwind.js";
|
|
19
|
-
import { findSections, classifySection, computeSizeHint, detectWidgets, brandHints } from "./ingest/semantic.js";
|
|
19
|
+
import { findSections, classifySection, computeSizeHint, detectWidgets, detectHoverEffects, brandHints } from "./ingest/semantic.js";
|
|
20
20
|
import { parseAbsoluteCanvas, canvasRoleSections, stripCdnSizePrefix } from "./ingest/canvas.js";
|
|
21
21
|
// Re-export the public surface so existing imports of "./persistence/html-ingest.js"
|
|
22
22
|
// (parseHtml, fetchHtml, and the IngestedAst/IngestedCanvas/CanvasElement/CanvasSection
|
|
@@ -95,6 +95,9 @@ export function parseHtml(html, detail = "compact", opts = {}) {
|
|
|
95
95
|
const sections = sectionEls.map((el) => {
|
|
96
96
|
const sec = classifySection(el, detail);
|
|
97
97
|
sec.size_hint = computeSizeHint(el, sec, styleBlocks);
|
|
98
|
+
const hover = detectHoverEffects(el);
|
|
99
|
+
if (hover.length)
|
|
100
|
+
sec.hover_effects = hover;
|
|
98
101
|
if (detail === "full") {
|
|
99
102
|
const widgets = detectWidgets(el, styleBlocks);
|
|
100
103
|
if (widgets.length)
|
|
@@ -114,13 +117,18 @@ export function parseHtml(html, detail = "compact", opts = {}) {
|
|
|
114
117
|
fonts: hints.fonts.length ? hints.fonts : undefined,
|
|
115
118
|
palette: hints.palette,
|
|
116
119
|
design_tokens: hints.design_tokens,
|
|
120
|
+
// Tailwind gradient utilities (Stitch CTA/hero backgrounds) reconstructed as
|
|
121
|
+
// linear-gradient strings — surfaced in BOTH modes since they're design-critical
|
|
122
|
+
// and the Play CDN never emits the resolved CSS.
|
|
123
|
+
gradients: hints.tailwind_gradients.length ? hints.tailwind_gradients : undefined,
|
|
117
124
|
background_images: hints.background_images.length ? hints.background_images : undefined,
|
|
118
125
|
warnings: warnings.length ? warnings : undefined,
|
|
119
126
|
};
|
|
120
127
|
if (detail !== "full")
|
|
121
128
|
return base;
|
|
122
|
-
// Full mode extras.
|
|
123
|
-
const
|
|
129
|
+
// Full mode extras: merge stylesheet gradients with the Tailwind ones (deduped).
|
|
130
|
+
const styleGradients = extractGradients(styleBlocks);
|
|
131
|
+
const gradients = [...new Set([...hints.tailwind_gradients, ...styleGradients])];
|
|
124
132
|
const result = {
|
|
125
133
|
...base,
|
|
126
134
|
gradients: gradients.length ? gradients : undefined,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { parse } from "node-html-parser";
|
|
5
5
|
import { extractStylesheetColors, extractStylesheetFonts, extractBackgroundImages, extractCssVarPalette, topColors, topFonts, mergeTopN, mergeTopNFonts, } from "./stylesheets.js";
|
|
6
|
-
import { resolveTailwindColors } from "./tailwind.js";
|
|
6
|
+
import { resolveTailwindColors, resolveTailwindGradients } from "./tailwind.js";
|
|
7
7
|
export const HEADING_TAGS = ["h1", "h2", "h3", "h4"];
|
|
8
8
|
// ─── section helpers ─────────────────────────────────────────────────────────
|
|
9
9
|
const BLOCK_TAGS = new Set(["div", "main", "section", "article", "header", "footer", "aside", "nav"]);
|
|
@@ -239,7 +239,11 @@ function pickCtas(el) {
|
|
|
239
239
|
});
|
|
240
240
|
el.querySelectorAll("a").forEach((a) => {
|
|
241
241
|
const cls = (a.getAttribute("class") ?? "").toLowerCase();
|
|
242
|
-
|
|
242
|
+
// An <a> is a CTA when it's named like one, OR styled like a button —
|
|
243
|
+
// Tailwind/Stitch pill buttons carry NO btn/button/cta class, just utilities
|
|
244
|
+
// (`px-8 py-4 rounded-xl bg-… …`): rounded + a fill/border + padding.
|
|
245
|
+
const styledButton = /\brounded(-|\b)/.test(cls) && /(\bbg-|bg-gradient|\bborder\b|\bborder-)/.test(cls) && /\b[pmg][xytrbl]?-/.test(cls);
|
|
246
|
+
if (/(btn|button|cta)/.test(cls) || styledButton) {
|
|
243
247
|
const t = a.text.trim();
|
|
244
248
|
const href = a.getAttribute("href") ?? undefined;
|
|
245
249
|
if (t)
|
|
@@ -633,11 +637,52 @@ export function brandHints(body, styleBlocks, googleFonts, tw) {
|
|
|
633
637
|
...(Object.keys(tw.fontFamily).length ? { font_family: tw.fontFamily } : {}),
|
|
634
638
|
}
|
|
635
639
|
: undefined;
|
|
640
|
+
const tailwind_gradients = tw ? resolveTailwindGradients(body, tw.colors) : [];
|
|
636
641
|
return {
|
|
637
642
|
colors,
|
|
638
643
|
fonts,
|
|
639
644
|
background_images,
|
|
640
645
|
palette: Object.keys(palette).length ? palette : undefined,
|
|
641
646
|
design_tokens: design_tokens && Object.keys(design_tokens).length ? design_tokens : undefined,
|
|
647
|
+
tailwind_gradients,
|
|
642
648
|
};
|
|
643
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Interaction effects present in a section's hover/transition utility classes
|
|
652
|
+
* (`hover:`/`group-hover:`/`active:`), normalized to the kinds Webcake can
|
|
653
|
+
* reproduce. The static AST otherwise drops every hover, so the cloned page has
|
|
654
|
+
* none — this lets the rebuild wire the matching Webcake hover behavior.
|
|
655
|
+
*/
|
|
656
|
+
export function detectHoverEffects(el) {
|
|
657
|
+
const set = new Set();
|
|
658
|
+
for (const node of el.querySelectorAll("[class]")) {
|
|
659
|
+
const cls = node.getAttribute("class");
|
|
660
|
+
if (!cls)
|
|
661
|
+
continue;
|
|
662
|
+
for (const t of cls.split(/\s+/)) {
|
|
663
|
+
const group = t.startsWith("group-hover:");
|
|
664
|
+
const h = t.startsWith("hover:") ? t.slice(6) : group ? t.slice(12) : t.startsWith("active:") ? t.slice(7) : null;
|
|
665
|
+
if (h === null)
|
|
666
|
+
continue;
|
|
667
|
+
if (/^scale-/.test(h))
|
|
668
|
+
set.add(group ? "image-zoom" : "scale");
|
|
669
|
+
else if (/^-?translate-y/.test(h))
|
|
670
|
+
set.add("lift");
|
|
671
|
+
else if (/^-?translate-x/.test(h))
|
|
672
|
+
set.add("slide");
|
|
673
|
+
else if (/^bg-/.test(h))
|
|
674
|
+
set.add("bg-color-change");
|
|
675
|
+
else if (/^text-/.test(h))
|
|
676
|
+
set.add("text-color-change");
|
|
677
|
+
else if (/^border/.test(h))
|
|
678
|
+
set.add("border-color-change");
|
|
679
|
+
else if (/^opacity-/.test(h))
|
|
680
|
+
set.add("fade");
|
|
681
|
+
else if (h === "underline")
|
|
682
|
+
set.add("underline");
|
|
683
|
+
else if (/^shadow/.test(h))
|
|
684
|
+
set.add("shadow");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return [...set];
|
|
688
|
+
}
|
|
@@ -189,6 +189,49 @@ const TW_COLOR_PREFIXES = [
|
|
|
189
189
|
const TW_PREFIX_RE = new RegExp(`^(?:${TW_COLOR_PREFIXES.join("|")})-(.+)$`);
|
|
190
190
|
// Tailwind's always-available keyword colors (present even without a config entry).
|
|
191
191
|
const TW_KNOWN = { white: "#ffffff", black: "#000000" };
|
|
192
|
+
/** Tailwind gradient direction utility → CSS gradient direction keyword. */
|
|
193
|
+
const TW_GRADIENT_DIR = {
|
|
194
|
+
t: "to top", tr: "to top right", r: "to right", br: "to bottom right",
|
|
195
|
+
b: "to bottom", bl: "to bottom left", l: "to left", tl: "to top left",
|
|
196
|
+
};
|
|
197
|
+
/** Resolve a color token to a value: config token, keyword color, or an arbitrary `[#hex]`/`[rgb(...)]` value. */
|
|
198
|
+
function resolveColorToken(tok, colors) {
|
|
199
|
+
const t = tok.split("/")[0]; // drop /80 opacity modifier
|
|
200
|
+
const arb = /^\[(.+)\]$/.exec(t);
|
|
201
|
+
if (arb)
|
|
202
|
+
return arb[1].replace(/_/g, " "); // arbitrary value: bg-[#ff7f78] / from-[rgb(0_0_0)]
|
|
203
|
+
return colors[t] ?? TW_KNOWN[t];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reconstruct `linear-gradient(...)` strings from Tailwind gradient utilities
|
|
207
|
+
* (`bg-gradient-to-br from-primary via-x to-y`) — Stitch uses gradient CTAs and
|
|
208
|
+
* hero backgrounds heavily, and Play-CDN never emits the resolved CSS, so the
|
|
209
|
+
* gradient would otherwise be invisible to the rebuild. Stops are resolved
|
|
210
|
+
* through the same color map (config token / keyword / arbitrary [#hex]).
|
|
211
|
+
*/
|
|
212
|
+
export function resolveTailwindGradients(body, colors) {
|
|
213
|
+
const out = new Set();
|
|
214
|
+
body.querySelectorAll("[class]").forEach((el) => {
|
|
215
|
+
const cls = el.getAttribute("class");
|
|
216
|
+
if (!cls)
|
|
217
|
+
return;
|
|
218
|
+
const utils = cls.split(/\s+/).map((c) => (c.includes(":") ? c.slice(c.lastIndexOf(":") + 1) : c));
|
|
219
|
+
const dirCls = utils.find((u) => /^bg-gradient-to-(t|tr|r|br|b|bl|l|tl)$/.test(u));
|
|
220
|
+
if (!dirCls)
|
|
221
|
+
return;
|
|
222
|
+
const dir = TW_GRADIENT_DIR[dirCls.replace("bg-gradient-to-", "")];
|
|
223
|
+
const stops = [
|
|
224
|
+
utils.map((u) => /^from-(.+)/.exec(u)?.[1]).find(Boolean),
|
|
225
|
+
utils.map((u) => /^via-(.+)/.exec(u)?.[1]).find(Boolean),
|
|
226
|
+
utils.map((u) => /^to-(.+)/.exec(u)?.[1]).find(Boolean),
|
|
227
|
+
]
|
|
228
|
+
.map((t) => (t ? resolveColorToken(t, colors) : undefined))
|
|
229
|
+
.filter((v) => !!v);
|
|
230
|
+
if (stops.length >= 2)
|
|
231
|
+
out.add(`linear-gradient(${dir}, ${stops.join(", ")})`);
|
|
232
|
+
});
|
|
233
|
+
return [...out];
|
|
234
|
+
}
|
|
192
235
|
/** Rank the config colors ACTUALLY used by utility classes in the body → resolved value list (usage-weighted). */
|
|
193
236
|
export function resolveTailwindColors(body, colors) {
|
|
194
237
|
const counts = new Map();
|
package/dist/smoke.js
CHANGED
|
@@ -176,6 +176,31 @@ console.log("== expand: relocates misplaced responsive.<bp>.animation into confi
|
|
|
176
176
|
console.log("== validate: accepts JSON string input ==");
|
|
177
177
|
const r3 = validatePage(JSON.stringify(good));
|
|
178
178
|
check("string input parsed & valid", r3.valid, r3.errors);
|
|
179
|
+
console.log("== validate: custom CSS/class/JS escape hatches (beyond-element capability) ==");
|
|
180
|
+
{
|
|
181
|
+
const clone = () => JSON.parse(JSON.stringify(good));
|
|
182
|
+
// Proper usage: customAdvance + declarations-only custom_css + page settings.extra_css/script → valid, no escape-hatch warnings.
|
|
183
|
+
const okPage = clone();
|
|
184
|
+
okPage.page[0].children[0].specials = { text: "CTA", customAdvance: true, custom_css: "background:linear-gradient(to right,#0058bc,#0070eb);box-shadow:0 20px 40px rgba(0,0,0,.08);", custom_class: "cta-pill,glow" };
|
|
185
|
+
okPage.settings.extra_css = "#w-btn1{transition:transform .3s}#w-btn1:hover{transform:scale(1.05)}";
|
|
186
|
+
okPage.settings.extra_script = "console.log('hi')";
|
|
187
|
+
okPage.settings.bhet = "<link href='https://fonts.googleapis.com/css2?family=Inter&display=swap' rel='stylesheet'>";
|
|
188
|
+
okPage.settings.bbet = "<script src='https://widget.example.com/chat.js'></script>";
|
|
189
|
+
const okR = validatePage(okPage);
|
|
190
|
+
check("escape hatch: valid page with custom_css/class + extra_css/script + bhet/bbet passes", okR.valid, okR.errors);
|
|
191
|
+
check("escape hatch: no false warning when used correctly", !okR.warnings.some((w) => /custom_css|customAdvance/.test(w)), okR.warnings.filter((w) => /custom_css|customAdvance/.test(w)));
|
|
192
|
+
// custom_css set but customAdvance missing → silent no-op warning.
|
|
193
|
+
const noAdvance = clone();
|
|
194
|
+
noAdvance.page[0].children[0].specials = { text: "CTA", custom_css: "box-shadow:0 2px 8px rgba(0,0,0,.1);" };
|
|
195
|
+
const naR = validatePage(noAdvance);
|
|
196
|
+
check("escape hatch: warns custom_css without customAdvance", naR.warnings.some((w) => /customAdvance!==true/.test(w)), naR.warnings);
|
|
197
|
+
// custom_css containing a selector/:hover → declarations-only warning.
|
|
198
|
+
const selInCss = clone();
|
|
199
|
+
selInCss.page[0].children[0].specials = { text: "CTA", customAdvance: true, custom_css: "#w-btn1:hover{transform:scale(1.1)}" };
|
|
200
|
+
const selR = validatePage(selInCss);
|
|
201
|
+
check("escape hatch: warns selector/:hover inside custom_css", selR.warnings.some((w) => /declarations inside/.test(w)), selR.warnings);
|
|
202
|
+
check("escape hatch: declarations-only misuse does NOT block (warning, not error)", selR.valid, selR.errors);
|
|
203
|
+
}
|
|
179
204
|
console.log("== expand: hydrates sparse nodes ==");
|
|
180
205
|
const sparse = {
|
|
181
206
|
page: [
|
|
@@ -434,6 +459,38 @@ check("tw: palette names every flattened token", twAst.palette?.["gray-900"] ===
|
|
|
434
459
|
check("tw: usage-ranked colors resolve nested + directional-border classes", (twAst.colors ?? []).includes("#2563eb") && (twAst.colors ?? []).includes("#111827") && (twAst.colors ?? []).includes("#3b82f6"), twAst.colors);
|
|
435
460
|
check("tw: design_tokens carries the resolved type scale", twAst.design_tokens?.font_size?.["lg"] === "18px" && twAst.design_tokens?.radius?.["full"] === "9999px", twAst.design_tokens);
|
|
436
461
|
check("tw: no config → extractTailwindConfig null", extractTailwindConfig("<div class='text-primary'>x</div>") === null);
|
|
462
|
+
console.log("== tailwind: gradient utilities + hover/transition effects (Stitch fidelity) ==");
|
|
463
|
+
const fxCfg = `<script id="tailwind-config">tailwind.config = { theme: { extend: { "colors": {
|
|
464
|
+
"primary": "#0058bc", "primary-container": "#0070eb", "secondary": "#fcd664", "surface": "#ffffff"
|
|
465
|
+
} } } }</script>`;
|
|
466
|
+
const fxPage = `<!DOCTYPE html><html><head>${fxCfg}</head><body>
|
|
467
|
+
<header class="bg-surface"><a class="text-primary hover:text-secondary transition-colors">Navigation menu link here</a></header>
|
|
468
|
+
<main>
|
|
469
|
+
<section class="bg-gradient-to-br from-primary to-primary-container"><h1 class="text-white">Join the affiliate program today</h1><p>Earn recurring commission from every referral you bring in.</p><a class="px-8 py-4 rounded-xl bg-gradient-to-br from-primary to-primary-container hover:scale-105 transition-transform" href="#">Get started now</a></section>
|
|
470
|
+
<section><h2 class="text-primary">Why partners choose us</h2>
|
|
471
|
+
<div class="group hover:-translate-y-1 transition-all"><img class="group-hover:scale-110" src="https://x/a.jpg"/><h3 class="group-hover:underline">Fast payouts</h3><p>Money in your account within days, not months.</p></div>
|
|
472
|
+
<div class="group hover:-translate-y-1 transition-all"><img class="group-hover:scale-110" src="https://x/b.jpg"/><h3>Real-time tracking</h3><p>Watch your referrals convert in a live dashboard.</p></div>
|
|
473
|
+
<div class="group hover:-translate-y-1 transition-all"><img class="group-hover:scale-110" src="https://x/c.jpg"/><h3>Dedicated support</h3><p>A partner manager helps you grow your revenue.</p></div>
|
|
474
|
+
</section>
|
|
475
|
+
</main>
|
|
476
|
+
<footer class="bg-primary"><a class="hover:opacity-80" href="#">Footer contact and company info link</a></footer>
|
|
477
|
+
</body></html>`;
|
|
478
|
+
const fxAst = parseHtml(fxPage, "compact");
|
|
479
|
+
check("fx: gradient utility reconstructed with resolved color stops", (fxAst.gradients ?? []).includes("linear-gradient(to bottom right, #0058bc, #0070eb)"), fxAst.gradients);
|
|
480
|
+
check("fx: gradients surfaced in COMPACT mode (design-critical)", (fxAst.gradients?.length ?? 0) >= 1, fxAst.gradients);
|
|
481
|
+
const heroFx = fxAst.sections.find((s) => s.role === "hero");
|
|
482
|
+
check("fx: hero hover scale captured", (heroFx?.hover_effects ?? []).includes("scale"), heroFx?.hover_effects);
|
|
483
|
+
const featFx = fxAst.sections.find((s) => s.role === "features");
|
|
484
|
+
check("fx: card lift captured", (featFx?.hover_effects ?? []).includes("lift"), featFx?.hover_effects);
|
|
485
|
+
check("fx: image-zoom (group-hover scale) captured", (featFx?.hover_effects ?? []).includes("image-zoom"), featFx?.hover_effects);
|
|
486
|
+
check("fx: underline (group-hover) captured", (featFx?.hover_effects ?? []).includes("underline"), featFx?.hover_effects);
|
|
487
|
+
const headFx = fxAst.sections.find((s) => s.role === "header");
|
|
488
|
+
check("fx: header hover text-color-change captured", (headFx?.hover_effects ?? []).includes("text-color-change"), headFx?.hover_effects);
|
|
489
|
+
// arbitrary-value gradient stop resolves too.
|
|
490
|
+
const arbGrad = parseHtml(`<!DOCTYPE html><html><head>${fxCfg}</head><body><main><section class="bg-gradient-to-r from-[#ff0000] to-primary"><h1>Heading text for the arbitrary gradient test case here</h1><p>Some descriptive paragraph text to clear the CSR shell threshold.</p></section></main></body></html>`, "compact");
|
|
491
|
+
check("fx: arbitrary [#hex] gradient stop resolved", (arbGrad.gradients ?? []).includes("linear-gradient(to right, #ff0000, #0058bc)"), arbGrad.gradients);
|
|
492
|
+
// no tailwind config → no gradients from this path, no hover noise on a plain page.
|
|
493
|
+
check("fx: plain page (no config, no hover classes) has no gradients/hover", (() => { const a = parseHtml(stylesheetHtml, "compact"); return a.gradients === undefined && a.sections.every((s) => s.hover_effects === undefined); })());
|
|
437
494
|
console.log("== ingest: full mode — blocks detection, gradients, images-as-objects ==");
|
|
438
495
|
const fullAst = parseHtml(stylesheetHtml, "full");
|
|
439
496
|
check("ingest: full mode palette present", fullAst.palette?.["primary"] === "#0A7C6E", fullAst.palette);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.74",
|
|
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",
|