agentleFS
educlopezeduclopez/mallard/skills/elementflow-kb/SKILL.md

elementflow-kb

Skill4 starsChanged 13 days agoMITView on GitHub

Curated knowledge base for ElementFlow — an Elementor-based page-builder theme/module for PrestaShop (backed by the `stsitebuilder` module: header, footer, home, category, product-page, and CMS-page builders, plus a widget library). Use when answering questions about ElementFlow-specific widgets (Newsletter, Container, Grid, PrestaShop module, Image, Registration/Login/ Sign-in forms, Custom template, Tabs & megamenu, Divider, Product name/ comment/gallery, Search, Text editor, Slider, Accordion), the page builders (product/category miniature, product/category page, shopping cart, header, mobile menu, CMS page), or ElementFlow-specific features (sticky header, wishlist, shortcodes, display conditions, drag & drop editor, sidebar/popup, side cart, SVG icons, checkout/my-account child theme, blog, JS events). Also trigger for install/upgrade/demo-import/dev-to-production questions about ElementFlow, or when the user mentions `stsitebuilder`, `st_site_builder`, or a project using the ElementFlow child theme. Trigger PROACTIVELY — even without an explicit ElementFlow question — whenever the working project is a PrestaShop store using ElementFlow/`stsitebuilder` (e.g. a `themes/*` child theme with `parent: classic` alongside a `modules/stsitebuilder/` directory) and the task touches storefront UI, layout, header/footer/home/category/ product-page structure, or theme CSS — check this KB before guessing at builder behavior or writing CSS that duplicates something the builder already handles natively. For PrestaShop platform-level questions unrelated to the builder (Symfony BO, Twig, Smarty core, hooks, theme.yml mechanics, migration 8→9) use the `prestashop-kb` skill instead. For the Panda theme (`st*` modules, SunnyToo, Easy Builder) use `panda-kb`.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Do NOT load for: PrestaShop core mechanics unrelated to the builder (use prestashop-kb), the Panda theme (use panda-kb), or non-PrestaShop …

    line 47

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

ElementFlow Knowledge Base

This skill exposes a curated ElementFlow knowledge base: 50 docs scraped from ElementFlow's own demo/docs site, plus empirically-learned gotchas from working on a real ElementFlow project (ps9-gaudibarcelonashop) that aren't documented anywhere official.

When this skill is relevant

Load and read from this KB when the user mentions:

  • ElementFlow as a theme/builder, or the stsitebuilder PrestaShop module (its Elementor-based engine — vendored under modules/stsitebuilder/libs/elementor/ in a project that uses it).
  • Any of its widgets by name (Newsletter, Container/Flex, Grid, PrestaShop module, Image, Registration form, Login form, Sign in, Custom template, Tabs & megamenu, Divider, Product name/comment/gallery, Search, Text editor, Slider, Accordion).
  • Its page builders (product/category miniature, product page, category page, shopping cart page, header, mobile menu, CMS page).
  • Its features (sticky header, wishlist, shortcodes {SSBC id=X}, display conditions, drag & drop editor behavior, sidebar/popup, side cart, SVG handling, checkout/my-account child theme, built-in blog, JS events).
  • Install/upgrade/demo-import/dev-to-production questions about a project running ElementFlow.

Do NOT load for: PrestaShop core mechanics unrelated to the builder (use prestashop-kb), the Panda theme (use panda-kb), or non-PrestaShop e-commerce.

KB layout

references/
├── README.md              # conventions + index
└── docs/
    ├── _index.md           # table of all 50 files by section, with source URLs
    ├── welcome.md, faqs.md, system-requirements.md, installation.md,
    │   upgrade.md, import-demo-data.md, setup-service.md,
    │   dev-to-production.md, changelog.md          # Getting started (9)
    ├── product-miniature.md, category-miniature.md,
    │   product-page-builder.md, category-page-builder.md,
    │   shopping-cart-page.md, header-builder.md,
    │   mobile-menu-builder.md, cms-page-builder.md  # Page builders (8)
    ├── widget-*.md                                  # Widgets (18)
    └── feature-*.md, howto-*.md                     # Features (15)

How to use the KB

  1. Start with the index: docs/_index.md for the full table, grouped by section, with the original source URL for each.
  2. Open the specific file for the widget/builder/feature in question.
  3. "How do I get ElementFlow running?" → docs/system-requirements.md, docs/installation.md, docs/import-demo-data.md, docs/dev-to-production.md in that order.
  4. Changelog / "what changed in vX" → docs/changelog.md.

Key facts to surface to agents

  • Server requirements: 512M+ memory_limit, 16MB post_max_size and upload_max_filesize.
  • Install: via Back Office → Modules → Upload a module (sitebuilder.zip), not composer/FTP-only. If widgets don't render after upload, clear cache via FTP as a fallback.
  • Upgrade: either the "Live Upgrade" one-click path or a manual package swap — either way, clear cache afterward.
  • Demo import: 4 distinct ways (Import feature, template Library, copy-from-live-frontend, copy-from-editor) — not just one.
  • Dev → production: domain change, full server transfer, and data-only transfer are three different documented workflows; moving from a subfolder install to root needs an explicit path fix.
  • Shortcodes: {SSBC id=X} reuses a saved widget/section across pages/templates — this is ElementFlow's answer to "reusable blocks."
  • Display conditions: widgets can be conditionally shown by customer group, cart state, product/category state, and conditions can nest.
  • Side cart: two different implementation paths exist (a single "Sidebar Cart" widget, vs. composing it from separate product/summary/button/voucher widgets) — check which one a given project actually uses before assuming.
  • PrestaShop module integration: ElementFlow ships specific compatibility patches for a number of popular third-party modules (SEO Audit, Gift card, Amazzing/Easy filter, WebP, PrestaShop Checkout, loyalty rewards, min/max quantity, product comparison, notify-me) — docs/feature-ps-module-integration.md lists exactly which and what the patch does.

Gotchas learned empirically (not in the official docs)

These came from working on a real ElementFlow project, not from the scraped docs — trust these over guessing from the docs alone when they conflict:

  • PrestaShop's CCC (combine/compress/cache) will happily bundle a stale copy of a theme's custom.css if PS_CSS_THEME_CACHE=1 and the cache wasn't cleared after a CSS rebuild. If a CSS fix "isn't showing up" after a rebuild, check themes/<theme>/assets/cache/theme-*.css for stale bundles before assuming the fix didn't apply.
  • Per-widget/global colors: ElementFlow's stsitebuilder DOES support real Elementor-style Global Colors (__globals__.text_color: "globals/colors?id=..." in a widget's stored settings), resolved via a separately auto-generated stylesheet (libs/elementor/js/elementor/css/elementor/css/post-global-setting-css-{shop_id}.css, regenerated automatically if missing) — NOT via a full "Kit" document/post the way vanilla Elementor works. Don't assume a __globals__ color reference is broken just because there's no dedicated kit post in the DB; check whether that global-setting stylesheet actually defines the referenced --e-global-color-* variable before concluding it's orphaned.
  • html,body{overflow-x:hidden} (a common "kill the mobile horizontal scroll" fix) combined with a height:100% reset on html/body (which Classic/child themes often carry for sticky-footer layouts) triggers a legacy CSS spec quirk: when one axis is non-visible and the other isn't set, the browser forces the other axis to auto — turning <body> into its own internally-scrolling box and silently breaking anything that listens to window/document scroll events (sticky headers, scroll-triggered JS). Use overflow-x: clip instead of hidden to sidestep this.
  • A widget's _css_classes / css_classes field (set via the builder's Advanced tab — _css_classes on widgets, css_classes on containers) is the correct way to give theme CSS a stable hook — prefer it over Elementor's auto-generated per-element IDs/classes (.elementor-element-xxxxxxx), which are long, unstable across re-saves, and hard to read. Set it from the very first CSS rule you write for an element, not "later once things settle" — a real project (llobetonline-style header+footer rebuild) wrote ~28 .elementor-element-{hash} selectors across header and footer before anyone flagged it, and untangling that afterward meant re-opening every widget in the editor to retrofit a class, then rewriting every selector in the theme CSS — a full-file refactor that a few seconds of Advanced-tab typing per widget, done up front, would have avoided entirely. When setting this via $e.run('document/elements/settings', ...) instead of clicking the panel, double-check which key the widget actually reads — passing css_classes to a widget (vs. a container) silently no-ops (the setting is accepted into the model and saves fine, but the class never lands in the rendered HTML) because widgets read _css_classes, not css_classes; verify by re-querying the live DOM (document.querySelector('.your-class')) after publish, not just by trusting that the $e.run call returned without error.
  • Header/footer/home content is genuinely split between builder-native config (colors, typography, per-widget custom CSS — set via the admin UI, stored in DB, wiped on a DB refresh from server) and theme CSS (structural/responsive/JS-driven behavior the builder UI can't express). When deciding where a fix belongs, ask which category it falls into — see the gitlab-project-bootstrap skill's sibling notes on this for the DB-refresh risk of builder-native settings.
  • A container's content_width setting (boxed vs full) changes the actual DOM shape, not just the visual width: with full, widgets render as direct children of the container; with the default/boxed setting, Elementor auto-wraps them in an extra .e-con-inner div first. A CSS selector written against one shape (e.g. .my-col > .elementor-widget) can silently match nothing on a sibling page/container that uses the other setting, even though both were built with "the same" container. Cover both shapes (.my-col .e-con-inner, .my-col > .elementor-widget { ... }) rather than assuming one.
  • Elementor's own auto-generated per-widget CSS (post-{id}.css) loads AFTER theme custom.css in <head>, and can carry the exact same selector specificity as a theme override (e.g. both are two classes deep). At equal specificity, source order wins — so the widget's own generated rule (commonly max-width:100% from a width/size setting) silently beats a theme CSS override that "should" apply. If a CSS property you set on a widget isn't taking effect despite the selector clearly matching, suspect this cascade order first and add !important rather than hunting for a specificity bug that isn't there.
  • A Grid-type container (container_type: grid) does not default its column-gap to zero — Elementor's own grid gap default leaves a visible strip of page background between columns even when both columns' own padding is zeroed. Set gap: 0 explicitly on the grid container if you want the columns to touch.
  • Uncaught TypeError: Cannot access offset of type string on string in libs/elementor/core/page-assets/data-managers/*/base.php:266 (font-icon-svg or any other page-assets data manager) is caused by a CORRUPTED on-disk cache file, not a widget-settings problem — don't reach for per-widget icon workarounds first. The file modules/stsitebuilder/libs/elementor/js/elementor/css/cache/_elementor_assets_data gets written by st_json_decode()/file_put_contents() with no file locking, so two concurrent PHP requests (e.g. a front-office page render racing an admin editor tab) can interleave their writes and leave valid JSON immediately followed by raw garbage text — st_json_decode() on that then hands back something get_saved_assets_data() can't array-index, and it throws on EVERY subsequent request that renders any font icon anywhere on the site. Symptoms: front-office pages 500 with an empty <head> (no CSS loads because the crash happens mid-render, before assets are printed), or the Element Flow editor hangs forever on its "LOADING" screen (check lando logs -s appserver for this exact fatal repeating many times in a row — that repetition is retries, not several different bugs). Fix: delete the file (gitignored, self-regenerating) — rm modules/stsitebuilder/libs/elementor/js/elementor/css/cache/_elementor_assets_data — then clear PrestaShop's own cache too (var/cache/prod) and reload; don't bother trying to hand-repair the JSON, a fresh render regenerates it cleanly.
  • content:url(...) on a CSS pseudo-element (::before/::after) is not a real replaced element for sizing purposes: it paints the image at its native pixel resolution regardless of any width/height set on the pseudo-element, and does NOT auto-preserve the image's intrinsic aspect ratio the way a real <img> does. This bites specifically when faking a builder-style layout on a page ElementFlow has no document for (e.g. PrestaShop-core password-recovery/forgot-password, which isn't a page you can open in the builder) and you reach for a pseudo-element to inject a logo or hero image via CSS alone. Use background-image + background-size: contain (or cover) on an empty (content:"") pseudo-element instead — that correctly rescales, same as object-fit does for a real <img>.
  • Faking a builder-style split/grid layout on a non-builder PrestaShop-core page (no ElementFlow document exists for it) means you're fighting Bootstrap, not Elementor: Bootstrap's .container sets an explicit width per breakpoint (capped well below the viewport even at its largest breakpoint), not just max-width — overriding max-width: none alone leaves it boxed with dead margins on wide screens; you also need width: 100%. Likewise .form-check/.form-check-label (Bootstrap checkboxes) carry legacy absolute-position offsets/padding-left that will double up with a flex gap if you re-lay them out — zero the old padding explicitly rather than just adding a gap on top of it.

_elementor_data is the source; .tpl and post-N.css are build artifacts

The single most expensive lesson: editing bs_st_site_builder_postmeta._elementor_data directly is only safe for pure content, never for structure or anything that emits CSS. Two separate generated artifacts go stale:

  • Almost every document type compiles to a per-language Smarty template under modules/stsitebuilder/views/templates/front/template/. Two different naming schemes coexist: miniatures are {id_lang}-{doc}-{type}.tpl (e.g. 1-8-60.tpl), while header/footer/mobile-menu/page docs are {id_shop}-{id_lang}-{doc}-{type}.tpl (e.g. 1-1-2-30.tpl, 1-2-2-30.tpl, 1-3-4-120.tpl). Only an editor publish regenerates them (page.php::render_template), and the front reads the .tpl, not the JSON — SiteBuilderBase.php:~1520 even prints "Template … not found." when one is missing. Consequence: after a raw DB edit the default language may look right while the others serve stale content. Always verify every language on the front, not just the default one. (When the files are absent entirely the front still renders live from data, so absence is not fatal — staleness is.)
  • post-{doc}.css is also only rebuilt on publish. Deleting a setting from _elementor_data does NOT remove the CSS rule it generated. Symptom: you remove e.g. _flex_size: grow in the DB and the layout is unchanged, which reads like a specificity bug but is a stale artifact.
  • An open Elementor editor autosaves and will silently clobber DB edits. Close the editor tab before touching _elementor_data, and re-read the row afterwards to confirm your write survived.
  • Workable pattern when you must bulk-edit: close the editor → write the DB → open the editor fresh (it loads your data) → Publish (regenerates every artifact for every language).

Flex layout inside a header container

  • Every child of a header row typically carries content_width: full, i.e. each asks for width: 100% of the row with flex: 0 1 auto. With two children they shrink to fit and it looks intentional; add a third and all three collapse to equal thirds. Worse, a middle child that demands 100% and refuses to shrink (_flex_size: grow, or shrink 0 without a content-based width) starves its siblings to width: 0 — a logo silently disappears and icon groups overlap.
  • The fix for a content-sized item between two flexible ones (e.g. a nav menu between logo and cart icons): on that container set Width = max-content (custom unit) plus _flex_size: 'custom' and _flex_shrink: 0. fit-content does NOT work — it still shrinks.
  • To place an element between existing siblings without dragging, use the builder's Order control (_flex_order: start | end | custom) on a sibling — e.g. setting the icons container to end turns logo|icons|menu into logo|menu|icons.

Automating the editor: what works and what doesn't

  • Repeater controls are hostile to scripted editing. Setting tabs / list item fields via injected JS repeatedly froze the renderer (CDP Runtime.evaluate timeouts), and a row duplicate that appeared in the DOM did not survive Publish. Non-repeater controls script fine. For bulk item edits prefer the DB route (then publish) or the clipboard route below.
  • Drag & drop cannot be automated. In the Structure navigator, dropping onto a container row always nests INSIDE it — never as a sibling — and dropping on a row boundary or on the parent row does not produce a sibling either. A canvas drag from the container grip does nothing. A human dropping on the boundary between two containers works fine.
  • Cross-environment element transfer without the system clipboard (the reliable alternative to both): ElementFlow's own "Paste from other site" needs a real cmd+V and fails on a synthetic one with "Make sure that both sites are updated to last version of Elementor…". Instead, note that Copy writes the payload to localStorage['elementor'].clipboard as {type:'elementor', siteurl:'<origin>/', elements:[…]}. Copy in the source, rewrite siteurl to the target origin, then in the target tab merge it in — const st=JSON.parse(localStorage.getItem('elementor')||'{}'); st.clipboard=payload; localStorage.setItem('elementor',JSON.stringify(st)); — and the normal Paste becomes enabled immediately, no reload needed. Gzip+base64 the payload and decode in-page with DecompressionStream('gzip') to keep it small. Paste always inserts INTO the right-clicked container as its last child, so paste the smallest piece you need and fix order with _flex_order.
  • Safety net when you have no DB access to the target (e.g. a staging server): the Theme builder row dropdown has Duplicar. Duplicate the document first — the copy lands active but unbound, so it renders nowhere — and if the edit goes wrong repoint the "Bind to pages" entry at the duplicate.

Menu widgets: two different link-type conventions

Both the header menu and the mobile menu can build links natively instead of hardcoding URLs, but they don't share the same schema — check which widget you're editing:

  • nested-tabs ("Tabs & Megamenu", the desktop navbar): per-item type uses word values — '' (default/custom) | category | information | my_account | cms | manufacturer (from SiteBuilderBase::getMenuTypes()), with conditional information_link / cms_link / category_link fields, plus tab_title_{iso} and tab_url_{iso}. The title is always taken from tab_title_*, whatever the type.
  • mobilemenu_list (mobile sidebar; controls come from menu_base.php::_reg_list_source): the repeater is called list and its per-item link_type uses numeric strings — 0 Custom | 1 Cms | 2 Manufacturer | 3 Category | 4 My account | 5 Information | 6 Product. Critically, its text_{iso} ("Custom title") and link_{iso} ("Custom link") are 'condition' => ['link_type' => '0'], so for any native type the widget resolves BOTH the URL and the label from the linked entity and ignores your custom title. If you need a label that differs from the entity's own name (e.g. "Novedades" rather than PrestaShop's "Nuevos productos"), you must use link_type: '0' and supply the per-language URLs yourself.
  • information link options are prices-drop, new-products, best-sales, stores, contact, sitemap (classes/data/element/Linker.php).

Desktop and mobile menus are separate documents

The mobile sidebar is its own builder document (type 120, typically id 4, "Mobile menu"), completely independent of the header — editing the navbar does nothing to it. On a fresh demo import it is full of demo data (nested accordions titled "Men" containing "Underwear / Undershirts / Pajamas / Collections"), which is what a client sees as "men men" in the burger menu. For a flat first-level menu, replace the demo nested-accordion with a single mobilemenu_list; keep the accordion only when you actually need expandable sub-levels. Also check the small label widget above it — on the demo it says "Categorías" with EN/CA left as "Add Your Text Here".

The sidebar background is dark (#1A1A1A) but widget text defaults to black, so a freshly added mobilemenu_list is invisible (measured 1:1 contrast). The demo widgets carried their colour in __globals__ rather than in a plain colour field, so it's easy to drop when hand-building settings. Set __globals__.mobile_menu_color = 'globals/colors?id=secondary' — in this theme secondary resolves to pure white (17.4:1 on the sidebar) — and the chevron SVG follows automatically because its fill inherits currentColor. The demo also set the colour only on the _mobile_extra responsive variant (mobile_menu_color_mobile_extra), which leaves the base breakpoint black; set the base key instead.

Driving the editor through Elementor's own JS API

Far more reliable than clicking the panel, and it goes through the model so it saves and regenerates CSS normally:

// find a widget by type, anywhere in the document
let target=null;
const iter=c=>c&&c.each&&c.each(m=>{ if(m.get('widgetType')==='mobilemenu_list') target=m; const e=m.get('elements'); if(e) iter(e); });
iter(elementor.documents.getCurrent().container.model.get('elements'));

const c = elementor.getContainer(target.get('id'));
$e.run('document/elements/select',   {container:c});
$e.run('document/elements/settings', {container:c, settings:{ text_color:'#FFFFFF99' }});
await $e.run('document/save/publish');            // publish without hunting for the button
elementor.documents.getCurrent().editor.isChanged // false once saved

elementor.getContainer(id) + $e.run('document/elements/settings', …) also sidesteps the repeater-freeze problem for non-repeater fields, and document/save/publish avoids coordinate-clicking a Publish button whose position shifts.

But document/elements/create cannot populate a NESTED widget's panels. nested-tabs owns its child containers (one per tabs repeater row) and creates them itself when rows are added through the UI. Passing a pre-populated elements array in the model, or calling create with the nested-tabs as the target container, both silently lose the children on save — in one case 6 panels came back as 1, and the layout setting was dropped too. What DOES survive: writing the children straight into _elementor_data (with the editor closed), then opening the editor fresh and publishing — verified with both a 7th navbar tab and 6 megamenu panels. So: settings via the JS API, nested children via the DB.

Building a megamenu panel that matches a two-column design

The desktop navbar is a nested-tabs with layout: menu; each top item's dropdown is just an empty container you fill. For the common "links on the left, image that swaps on hover on the right" design, nest a second nested-tabs inside that container — no custom JS needed:

  • layout: 'tabs', open_on: 'hover'
  • tabs_direction: 'inline-start' → titles column to the LEFT of the content (block-start/block-end/inline-end/inline-start; the default puts titles above)
  • tabs_heading_direction: 'column' → titles stack vertically
  • tabs_width: {unit:'px', size:280} → left column width (emits --n-tabs-heading-width)
  • link look: title_typography_* group (plus title_typography_hover / _active), tabs_title_padding, tabs_title_align_items, tabs_title_space_between
  • one child container per tab = the right-hand panel: background image cover/center, min_height = panel height, flex_justify_content: 'flex-end' to sit content at the bottom, and a gradient overlay (background_overlay_background:'gradient', background_overlay_color:'rgba(0,0,0,0.6)', background_overlay_color_b:'rgba(0,0,0,0)', background_overlay_gradient_angle: 0deg) for the caption legibility.

Reference relative image URLs (/img/c/10-category_cover.jpg) rather than absolute ones so the same config works in local and staging.

Watch the dropdown-width trap: the dropdown can never be wider than the menu widget's own container. If you gave that container width: max-content to stop it starving its siblings, you have also capped the dropdown. submenu_width: '1' ("stretch to full width") is documented as working conditionally and does nothing in that case. The fix is to invert which children are content-sized: give the logo and icon containers width: max-content + _flex_size:'custom' + _flex_shrink: 0, and let the menu container stay full-width with flex_justify_content: 'center'. Nothing collapses, and the dropdown gets the full menu width.

Titles vs linked entities differ between the two menu widgets (see the section above): in nested-tabs, EBB::textAndLinkLang('tab_title', …) wins and the entity name is only a fallback, so you can label a tab "Muebles" while it links to a category actually named "Mobiliario". In mobilemenu_list you cannot.

Gaps to be honest about

  • Most of these 50 docs are short FAQ-style tips ("here's how to fix X specific problem"), not exhaustive settings references. For the real, complete list of controls/options a widget exposes, the source of truth is the vendored Elementor widget PHP in the actual project (modules/stsitebuilder/libs/elementor/includes/widgets/*.php), not this KB.
  • The doc site (elementflow.io) has no crawlable sitemap — the full page list only appears in the client-side-rendered sidebar nav, not in each page's static HTML. If ElementFlow ships new doc pages later, re-discover them via a real browser render (not a plain HTTP fetch) before assuming this index is complete.
  • Screenshots referenced in the original posts were not captured — where a doc says "see the screenshot below," that visual context is missing here.

Cross-skill pointer

For the PrestaShop platform mechanics ElementFlow sits on top of (hooks lifecycle, theme.yml/parent-child cascade, Symfony BO, Smarty syntax, migration 8→9), see the sibling skill prestashop-kb. For GitLab project hygiene on an ElementFlow-based client repo, see gitlab-project-bootstrap.

Megamenú con nested-tabs: el desplegable no puede ser más ancho que su columna (y submenu_width no lo arregla)

En layout: menu, el panel del dropdown es .e-n-tabs-content con position: absolute, y su bloque contenedor es el container que envuelve al widget (Elementor pone position: relative en todos los .e-con). Si el widget vive en una columna de 985px, el panel se queda en 985px hagas lo que hagas: submenu_width no tiene efecto en este layout, y _position: absolute sobre el container hijo del panel tampoco (Elementor no emite CSS de posicionado para containers con ese control; queda position: relative).

La única salida es mover el bloque contenedor hacia arriba con CSS: position: static en la columna del menú → el bloque contenedor pasa a ser la fila del header, y ahí ya puedes centrar el panel con width: min(calc(100% - 24px), Npx); left: 50%; transform: translateX(-50%). Comprueba offsetParent del .e-n-tabs-content para saber contra qué estás midiendo antes de escribir números: al cambiar el position de un ancestro, top/left se recalculan contra otro elemento y el panel puede acabar solapando el header (necesitarás top: 100%).

Fondo de un container: sin background_size: cover la foto sale a tamaño natural

Al poner una imagen de fondo en un container (por ejemplo el panel de un tab que hace de "foto grande"), el background-size por defecto deja la imagen a su tamaño natural centrada, con el fondo del container asomando a los lados. Parece un bug de anchura y no lo es: background_size: 'cover' + background_position: 'center center' es nativo y es lo que equivale al object-fit: cover de un <img>.

post-{doc}.css gana a custom.css por orden de fuente

Ya está anotado más arriba para widgets; aplica igual a las variables/paddings que emiten los nested-tabs. Si pones un padding en un elemento interno del widget desde el tema y el computed sigue mostrando el valor del builder, no busques un fallo de especificidad: son iguales y el generado carga después. !important es la respuesta correcta aquí.

Publicar por $e.run('document/save/publish') a veces no resuelve

El await puede colgarse (timeouts de Runtime.evaluate a los 45s) aunque los settings sí se hayan aplicado al modelo. No asumas que falló: vuelve a leer elementor.documents.getCurrent().editor.isChanged en una llamada aparte; si sigue true, relanza el publish. Y ojo al navegar fuera del editor: un beforeunload bloquea la navegación si quedan cambios sin guardar.

tabs_justify_vertical es el control de alineación vertical de los títulos

Con tabs_heading_direction: column, el control que alinea la lista de títulos arriba/centro/abajo es tabs_justify_vertical (start|center|end|stretch), NO tabs_justify_content (ése está condicionado a direction row). Su diccionario de selectores reescribe también --n-tabs-title-flex-basis, así que al cambiarlo se pierde la separación que dieran los paddings: recupérala con tabs_title_space_between.

Las miniaturas de categoría/producto de PrestaShop pueden traer relleno blanco — no las uses como imagen de fondo

Si la tienda tiene la generación de miniaturas en modo "fit" con relleno (lo habitual en las importaciones de demo), los ficheros img/c/{id}-category_cover.jpg, -category_default.jpg, etc. contienen la foto original centrada sobre un lienzo blanco del tamaño nominal. Al usar uno de esos ficheros como background_image de un container con background-size: cover, el relleno se escala con la foto: se ve la imagen pequeña en el centro con franjas a los lados, y el síntoma se confunde con "el contenedor es más ancho de lo que debería" o "el cover no se aplica".

Comprueba siempre el fichero en disco (sips -g pixelWidth -g pixelHeight) y compáralo con lo que se ve renderizado; si el ratio del contenido visible no coincide con el ratio del fichero, hay relleno. La única sin relleno es la original (img/c/{id}.jpg).

Migrar header/footer/cualquier doc de ElementFlow a otro entorno (server nuevo, DB separada): 5 fallos distintos, todos silenciosos

Sincronizar el _elementor_data de un doc (header, footer, etc.) de un entorno a otro vía DB directa — porque el server destino se aprovisionó aparte y no comparte DB con local — puede fallar de 5 formas independientes, ninguna de las cuales lanza un error visible (con _PS_MODE_DEV_ apagado, que es lo normal en un server): el front simplemente cae al header/footer nativo de PrestaShop Classic sin avisar, o el widget se ve pero con un recurso roto. Cada una se diagnostica y arregla distinto — comprobar las 5 antes de dar por bueno un deploy cross-entorno:

  1. El binding del doc (ST_SITE_B_HEADER_BIND / ST_SITE_B_FOOTER_BIND en ps_configuration) puede no existir en el entorno destino si ese header/footer se activó ahí en un momento distinto al de origen, o si el entorno es más nuevo. Sin esa fila, getHeaderFooterData() devuelve false de entrada y ni intenta renderizar — compara SELECT name FROM ps_configuration WHERE name LIKE 'ST_SITE_B%' entre ambos entornos antes de asumir que el problema es el contenido.
  2. Copiar _elementor_data vía mysql ... < archivo.sql generado con QUOTE(meta_value) puede corromper el JSON por doble-escapado si el sql_mode/charset de la sesión que genera el QUOTE() no coincide exactamente con el de la sesión que aplica el UPDATE — el resultado son bytes de más (backslashes duplicados) y json_decode falla. Ojo con el propio diagnóstico: volcar la columna con mysql -N -e "SELECT ..." para inspeccionarla a mano TAMBIÉN escapa backslashes/saltos de línea en la salida de terminal por defecto — así que un \\" que ves en un dump así puede ser un artefacto de la CLI, no corrupción real. Para saber si el dato en DB es válido de verdad, decodifícalo con PHP (json_decode($val) leído por mysqli, no por la CLI de mysql). Para transferir sin ningún riesgo de escapado: exporta con PHP a base64 (base64_encode($val)), pasa el .b64 como texto plano, decodifica y aplica en destino con una prepared statement (bind_param('s', ...)) — nunca reconstruyendo un literal SQL a mano.
  3. La propia caché JS de ElementFlow (libs/elementor/assets/js/js-loader.php + los *.bo.js con nombre hash) puede llegar al server con el dominio de origen horneado dentro, si ese server se aprovisionó copiando ficheros de una máquina de desarrollo en vez de con un despliegue limpio (aunque estos ficheros estén en .gitignore y no viajen por git, pueden colarse por otra vía — rsync, zip, snapshot). Síntoma: en el editor del BO del server nuevo, la consola muestra peticiones a JS fallando contra el dominio de local (net::ERR_FAILED, CORS ... loopback address space). Se arregla borrando js-loader.php y los *.bo.js — se regeneran solos con el dominio correcto en la siguiente carga.
  4. Un widget de imagen (logo, etc.) puede tener la URL guardada como absoluta al dominio de origen dentro del propio _elementor_data, no solo en caché — Elementor guarda src como URL completa cuando subes la imagen desde el media manager. Si sincronizas el dato tal cual a otro dominio, el <img> referencia una URL cruzada que el navegador bloquea silenciosamente (naturalWidth: 0, complete: true, sin error de red visible salvo que mires la consola). Antes de dar por bueno un logo/imagen tras una migración, comprueba document.querySelector('img').src — si apunta al dominio viejo, hazle un str_replace() del dominio dentro del JSON (mismo método seguro del punto 2: leer con mysqli, reemplazar, json_decode para verificar, aplicar con prepared statement) en vez de reeditar el widget a mano.
  5. Los módulos nativos de PrestaShop (ps_linklist, ps_customeraccountlinks, ps_contactinfo, blockwishlist, etc.) pueden seguir enganchados a displayFooter/displayHeader en el entorno nuevo si nunca se desengancharon ahí — esto es un dato de módulo (ps_hook_module), no algo que viaje con el _elementor_data del doc. Resultado: el footer/header de ElementFlow y el nativo de PrestaShop se renderizan los dos a la vez, uno debajo del otro. Se corrige llamando a $module->unregisterHook('displayFooter') (o el hook que toque) módulo por módulo — no aparece como error, solo como contenido duplicado.

Método general para diagnosticar sin acceso a logs de servidor (cuando _PS_MODE_DEV_ está apagado y no hay permisos de root para leer el log de PHP-FPM): escribe un script PHP suelto que haga require_once config/config.inc.php y llame directamente a la función que falla (p. ej. $module->getBuilderContent($data)) envuelta en try/catch (\Throwable $e) con error_reporting(E_ALL) y display_errors=1 — ejecútalo con php <ruta> desde el propio server. Cuidado: un script así bootstrapeado a pelo (sin pasar por el front controller real) puede no tener cargada la librería Elementor (Elementor\Widget_Base no existe todavía) y lanzar un error que no reproduce el fallo real — si ves ese error concreto, llama primero a $module->elementPlugin() a mano en el script antes de la función que quieres probar; si el error cambia a algo tipo "getPageName() on null", es un artefacto del bootstrap mínimo (falta el Context::getContext()->controller real), no el bug real — en ese caso, mejor activar _PS_MODE_DEV_ temporalmente y mirar el HTML real servido (con cuidado, ver más abajo), o depurar por descarte comprobando cada una de las 5 causas de esta sección directamente en la DB.

Cuidado al activar _PS_MODE_DEV_ temporalmente en un server para depurar: los avisos "Deprecated"/"Warning" que imprime PHP se cuelan dentro de respuestas que se esperan como JS/JSON puro (por ejemplo ?stsitebuilder=1&eb_id=...), rompiéndolas con Uncaught SyntaxError: Unexpected token '<' — es decir, activar dev mode para depurar puede romper activamente el editor mientras está activo. Revierte inmediatamente después de mirar, no lo dejes puesto mientras sigues investigando otra cosa.

El mismo widget puede salir position: relative en un entorno y no en otro

Un nested-tabs idéntico (mismos settings, replicado por clipboard) generó position: relative sobre el propio widget en un entorno y no en otro. Si tu CSS depende de cuál es el bloque contenedor de un descendiente absoluto (por ejemplo un dropdown de megamenú), eso significa que el layout funciona en local y se rompe en el servidor sin que haya diferencia de datos aparente.

No confíes en heredar el position que "toca": fuerza explícitamente a static todos los ancestros que quieras saltar (!important, porque el relative lo pone el CSS generado del widget, que carga después del tema), y verifica con element.offsetParent en cada entorno en lugar de asumir la cadena que viste en local.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.