webcake-landing-mcp 1.0.55 → 1.0.57
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/README.md +8 -3
- package/dist/changelog.json +14 -14
- package/dist/domains/landing/elements/commerce.js +103 -21
- package/dist/domains/landing/elements/content.js +97 -39
- package/dist/domains/landing/elements/form.js +100 -46
- package/dist/domains/landing/elements/layout.js +56 -38
- package/dist/domains/landing/elements/marketing.js +90 -61
- package/dist/domains/landing/guide.js +20 -5
- package/dist/domains/landing/index.js +96 -1
- package/dist/domains/landing/instructions.js +3 -3
- package/dist/domains/landing/validate.js +261 -1
- package/dist/domains/landing/vocab.js +3 -3
- package/dist/persistence/config.js +12 -3
- package/dist/persistence/html-ingest.js +460 -62
- package/dist/persistence/webcake-client.js +154 -10
- package/dist/smoke.js +257 -0
- package/dist/tools/ingest.js +30 -9
- package/dist/tools/media.js +154 -0
- package/dist/tools/persistence.js +34 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,6 +152,11 @@ npx -y webcake-landing-mcp login # opens the browser once, saves the token to
|
|
|
152
152
|
|
|
153
153
|
…or set `WEBCAKE_ENV` (`local` | `staging` | `prod` — fills in all base URLs) + `WEBCAKE_JWT`.
|
|
154
154
|
|
|
155
|
+
For `publish_page` to produce a **rendered** (non-blank) page, a build host is needed:
|
|
156
|
+
- `prod` preset auto-configures `https://build.webcake.io` — no extra setup.
|
|
157
|
+
- For staging/local, set `WEBCAKE_BUILD_BASE=<url>` or send the `x-webcake-build-base` header per request.
|
|
158
|
+
- Without it, `publish_page` falls back to source-only with `rendered:false` + a warning.
|
|
159
|
+
|
|
155
160
|
Everything else — the full env-var table, environment presets, per-request headers for the hosted
|
|
156
161
|
server, the `login` browser flow (+ backend contract), and how to grab a JWT by hand — lives in
|
|
157
162
|
**[docs/configuration.md](docs/configuration.md)**.
|
|
@@ -164,7 +169,7 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
|
|
|
164
169
|
|-------|---------------|
|
|
165
170
|
| **[Connect your IDE / claude.ai](docs/connect-mcp.md)** | Step-by-step connection for every client (npx & hosted URL), troubleshooting table. |
|
|
166
171
|
| **[Configuration](docs/configuration.md)** | Env vars, `--env` presets, browser `login`, per-request headers, getting a JWT. |
|
|
167
|
-
| **[Tools reference](docs/tools.md)** | All
|
|
172
|
+
| **[Tools reference](docs/tools.md)** | All 20 tools in detail + the step-by-step workflow + model notes. |
|
|
168
173
|
| **[Usage examples](docs/usage-examples.md)** | Three end-to-end walkthroughs: build from a brief, surgical edit, inspect a type. |
|
|
169
174
|
| **[Manual / advanced install](docs/manual-install.md)** | Shell installers, cloned builds, hand-written per-IDE config. |
|
|
170
175
|
| **[Page-element schema](docs/page-element-schema.md)** | The full element-model reference (+ [every special/event](docs/element-specials-reference.md)). |
|
|
@@ -173,13 +178,13 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
|
|
|
173
178
|
|
|
174
179
|
## 🧰 The tools at a glance
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
20 tools in five groups — full descriptions in **[docs/tools.md](docs/tools.md)**:
|
|
177
182
|
|
|
178
183
|
| Group | Tools | Needs |
|
|
179
184
|
|-------|-------|-------|
|
|
180
185
|
| **Reference** | `get_generation_guide` · `list_elements` · `get_element` · `get_page_schema` | nothing |
|
|
181
186
|
| **Generation** | `new_element` · `new_page_skeleton` · `validate_page` | nothing |
|
|
182
|
-
| **Media** | `search_images` (real Pexels stock photos)
|
|
187
|
+
| **Media** | `search_images` (real Pexels stock photos) · `upload_images` (re-host external images) | nothing |
|
|
183
188
|
| **Ingest** | `ingest_html` · `ingest_url` (recreate an existing page) | nothing |
|
|
184
189
|
| **Persistence** | `list_organizations` · `create_page` · `list_pages` · `find_pages` · `get_page` · `update_page` · `add_section` · `patch_page` · `publish_page` | `WEBCAKE_API_BASE` + `WEBCAKE_JWT` |
|
|
185
190
|
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.57",
|
|
4
|
+
"d": "11/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "New upload_images tool re-hosts up to 20 external image URLs or data: URIs as Webcake-hosted URLs (statics.pancake.vn) by downloading and uploading…",
|
|
7
|
+
"vi": "Công cụ upload_images mới tải lại tối đa 20 URL ảnh ngoài hoặc data: URI thành URL do Webcake lưu trữ (statics.pancake.vn) bằng cách tải về và…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.56",
|
|
11
|
+
"d": "11/06/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "The expand pipeline now automatically derives styles.background from specials.src for every image-block node; the live published renderer reads only…",
|
|
14
|
+
"vi": "Pipeline expand nay tự động tính styles.background từ specials.src cho mọi node image-block; renderer trên trang published chỉ đọc…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.55",
|
|
4
18
|
"d": "10/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "validate_page now errors when an element type that the renderer cannot animate (any type other than group, image-block, text-block, rectangle,…",
|
|
28
42
|
"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,…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.51",
|
|
32
|
-
"d": "10/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "create_page, update_page, and add_section dry-run responses now include a draft_id, and all three tools now accept draft_id as an input parameter:…",
|
|
35
|
-
"vi": "Các response dry-run của create_page, update_page và add_section nay đều trả về draft_id, đồng thời cả ba công cụ đều nhận draft_id làm tham số đầu…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.50",
|
|
39
|
-
"d": "10/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "New publish_page tool makes a page live: reads the page's current stored source, saves it as a new version, and creates or updates the…",
|
|
42
|
-
"vi": "Công cụ publish_page mới giúp đưa trang lên live: đọc source đang lưu của trang, lưu thành phiên bản mới và tạo hoặc cập nhật bản ghi…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -2,22 +2,60 @@ import { seedPosition, setStyle, setBox } from "../../../core/element.js";
|
|
|
2
2
|
export const COMMERCE = [
|
|
3
3
|
{
|
|
4
4
|
type: "list-product", category: "commerce", container: false, defaultName: "ListProduct",
|
|
5
|
-
summary: "Product list bound to the page store; clicking a card opens the popup-checkout overlay.",
|
|
5
|
+
summary: "Product list bound to the page store; clicking a card opens the popup-checkout overlay configured by page-level cartConfigs.checkoutConfig.",
|
|
6
6
|
useWhen: "Show purchasable products.",
|
|
7
7
|
keySpecials: {
|
|
8
|
+
// --- filter / layout ---
|
|
8
9
|
select: "'product' | 'tag' | 'category' — which dimension to filter products by.",
|
|
9
|
-
type: "'expect' | 'except' — treat the expect*/except* arrays as an allowlist (expect) or denylist (except).",
|
|
10
|
-
expect: "array of product id strings to include (select='product', type='expect').",
|
|
10
|
+
type: "'expect' | 'except' — treat the expect*/except* arrays as an allowlist (expect) or denylist (except). FOOTGUN: type='expect' + select='product' + empty expect array renders ZERO products (empty allowlist = nothing; category/tag allowlists treat empty differently). SSR caps at 200 products.",
|
|
11
|
+
expect: "array of product id strings to include (select='product', type='expect'). Must be non-empty when type='expect', otherwise no products render.",
|
|
11
12
|
except: "array of product id strings to exclude (select='product', type='except').",
|
|
12
13
|
expectCategory: "array of category ids to include (select='category').",
|
|
13
14
|
exceptCategory: "array of category ids to exclude (select='category').",
|
|
14
15
|
expectTags: "array of tag slugs to include (select='tag').",
|
|
15
16
|
exceptTags: "array of tag slugs to exclude (select='tag').",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
direction: "'column' | (other) — 'column' = whole card is one click target; otherwise thumbnail + cart button each get handlers (and remain-quantity shows).",
|
|
17
|
+
direction: "'column' | (other) — layout direction. With the SSR wrapper every card is fully clickable in BOTH directions; thumbnail+cart-only handlers are a legacy path.",
|
|
18
|
+
row_layout: "'layout_1' | 'layout_2' — row-direction card variant (row only).",
|
|
19
19
|
numerical_order: "boolean — show numbered labels (01, 02 …) on thumbnails.",
|
|
20
|
-
|
|
20
|
+
// --- title / price ---
|
|
21
|
+
format_title: "'default' | 'sku' | 'sku-name' | 'name-category' — product title composition.",
|
|
22
|
+
format_price: "'range' | 'discount' | 'discount-revert' — 'discount' shows % off badge when original > retail; 'discount-revert' reverses the cost/price order + badge; any non-'range' value shows the first variation's original+retail pair.",
|
|
23
|
+
// --- quantity / stock ---
|
|
24
|
+
show_remain_quantity: "boolean — show remaining stock count (default true).",
|
|
25
|
+
remain_quantity_text: "string — low-stock label; {{value}} replaced with actual count.",
|
|
26
|
+
is_runout: "boolean — hide sold-out products.",
|
|
27
|
+
// --- rating / social proof ---
|
|
28
|
+
show_rating_star: "boolean — show fake star rating derived from product name. WARNING: if any product has a null name this throws at product.name.charCodeAt and crashes the build for that card.",
|
|
29
|
+
show_total_sold: "boolean — show total sold count.",
|
|
30
|
+
// --- image ---
|
|
31
|
+
hide_product_image: "boolean — hide product thumbnail.",
|
|
32
|
+
thumbnail_size: "number (px, default 195) — thumbnail width.",
|
|
33
|
+
thumbnail_height: "number (px) — thumbnail height override.",
|
|
34
|
+
background_size: "'cover' | 'contain' | … (default 'cover') — thumbnail background-size.",
|
|
35
|
+
// --- button ---
|
|
36
|
+
show_add_button: "'show' | 'hide' | 'hide_icon' (default 'show') — add-to-cart button visibility.",
|
|
37
|
+
text_add_button: "string — custom add-to-cart button label.",
|
|
38
|
+
iconCart: "url string — custom cart icon for the button.",
|
|
39
|
+
btn_width: "number (px, default 182) — button width.",
|
|
40
|
+
btn_height: "number (px) — button height.",
|
|
41
|
+
btn_border: "string — button border shorthand.",
|
|
42
|
+
border_type_btn: "string — button border type.",
|
|
43
|
+
border_width_btn: "number — button border width.",
|
|
44
|
+
// --- SKU / extras ---
|
|
45
|
+
show_extra_sku: "boolean — show additional SKU attributes on the card.",
|
|
46
|
+
// --- typography / font ---
|
|
47
|
+
font_family: "string — card font family.",
|
|
48
|
+
textAlign: "'left' | 'center' | 'right' — card text alignment.",
|
|
49
|
+
boldName: "boolean — bold product name.",
|
|
50
|
+
boldExtraTitle: "boolean — bold extra SKU title.",
|
|
51
|
+
list_product_font: "object — per-element font sizes (px numbers): { pName, pPrice, pOrgPrice, pQuantity, pButton, pStarSize, pTotalSold }.",
|
|
52
|
+
// --- flash sale ---
|
|
53
|
+
show_flash_sale_icon: "boolean (default true) — show flash-sale badge icon.",
|
|
54
|
+
// --- checkout overlay ---
|
|
55
|
+
// NOTE: the checkout overlay itself is configured by page-level
|
|
56
|
+
// cartConfigs.checkoutConfig (show_description / show_attrs /
|
|
57
|
+
// show_quantity / popup_position …), NOT by list-product specials.
|
|
58
|
+
// Only format_price passes through to the overlay from here.
|
|
21
59
|
},
|
|
22
60
|
seed: (el) => {
|
|
23
61
|
seedPosition(el);
|
|
@@ -26,11 +64,21 @@ export const COMMERCE = [
|
|
|
26
64
|
el.specials.format_title = "sku";
|
|
27
65
|
el.specials.numerical_order = true;
|
|
28
66
|
},
|
|
67
|
+
// Styles consumed beyond colorBtn (all exportCss.js):
|
|
68
|
+
// colorIconBtn, colorBtnBorder, colorTextBtn, colorBgIcon,
|
|
69
|
+
// colorTitle, colorPrice, colorQuantity, colorBg, colorFlashSale,
|
|
70
|
+
// colorTextSale, borderRadius.
|
|
29
71
|
},
|
|
30
72
|
{
|
|
31
73
|
type: "search-list-product", category: "commerce", container: false, defaultName: "SearchListProduct",
|
|
32
|
-
summary:
|
|
33
|
-
|
|
74
|
+
summary: [
|
|
75
|
+
"Search box + product results overlay. Requires a co-existing list-product element on the page — openProduct() delegates to a list-product vm, so without one nothing opens.",
|
|
76
|
+
"CRASH: clicking search hard-crashes when product sync is absent (window.sync.products.findIndex, search-list-product.js:51) — page must have synced products.",
|
|
77
|
+
"Placeholder text and button label are NOT settable — they are locale-fixed by currency.",
|
|
78
|
+
"background + color style the SEARCH BUTTON only; borderRadius splits input-left / button-right.",
|
|
79
|
+
"A single search hit skips the results popup and opens the checkout overlay directly.",
|
|
80
|
+
].join(" "),
|
|
81
|
+
useWhen: "Searchable catalog (pair it with a list-product element and ensure product sync is active).",
|
|
34
82
|
keySpecials: {},
|
|
35
83
|
seed: (el) => {
|
|
36
84
|
seedPosition(el);
|
|
@@ -41,8 +89,14 @@ export const COMMERCE = [
|
|
|
41
89
|
},
|
|
42
90
|
{
|
|
43
91
|
type: "cart-items", category: "commerce", container: false, defaultName: "CartItems",
|
|
44
|
-
summary:
|
|
45
|
-
|
|
92
|
+
summary: [
|
|
93
|
+
"DO NOT PLACE — renders an empty string on the published page.",
|
|
94
|
+
"The publish renderer's type-switch has no cart-items case (index.js default ''), and render_v4 has no cart-items class.",
|
|
95
|
+
"The real cart-items UI is WCart's own floating drawer (div.cart_view) appended beside the cart icon (CartView.ts:172,249; floating.ts:164) — it is positioned by cart settings, not by this element.",
|
|
96
|
+
"Editor shows a placeholder product card; live page shows nothing (editor ≠ live).",
|
|
97
|
+
"The page-level cartConfigs.checkoutElements['CART-ITEM'] (itemNameSize / itemPriceSize / inputQuantitySize) styles the FLOATING DRAWER — values must be CSS strings WITH units (e.g. \"14px\"); bare numbers produce invalid CSS.",
|
|
98
|
+
].join(" "),
|
|
99
|
+
useWhen: "Do NOT place this element. Configure cartConfigs.checkoutElements['CART-ITEM'] for drawer font sizes instead.",
|
|
46
100
|
keySpecials: {},
|
|
47
101
|
seed: (el) => {
|
|
48
102
|
seedPosition(el);
|
|
@@ -51,11 +105,18 @@ export const COMMERCE = [
|
|
|
51
105
|
},
|
|
52
106
|
{
|
|
53
107
|
type: "cart-quantity", category: "commerce", container: false, defaultName: "Cart Quantity",
|
|
54
|
-
summary:
|
|
55
|
-
|
|
108
|
+
summary: [
|
|
109
|
+
"Quantity stepper (+/-) for a product variation group.",
|
|
110
|
+
"Value always starts at 1 (hardcoded); minus clamps at 1; plus is unbounded.",
|
|
111
|
+
"Publishes <id>__quantity-change on each click so WCart can read input[type=number] in the variation group DOM.",
|
|
112
|
+
"ignoreOnHidden is DEAD CODE on live — the show/hide dispatcher only handles checkbox-group / select / radio / quantity_input; cart-quantity is not in that list, so hiding this element never suppresses its quantity contribution.",
|
|
113
|
+
"field_name path: the SSR input has no name attribute; quantity flows via WCart reading input[type=number] in the sprod variation group (WCart path — works with any field_name value, including the seeded cart_quantity_<id>).",
|
|
114
|
+
"The form quantity-LINK path (linkType / prodId|variationId) requires field_name==='quantity' AND field_type==='number' exactly — the seeded cart_quantity_<id> never satisfies that; only change field_name to 'quantity' when using the explicit form-link path, not the WCart group path.",
|
|
115
|
+
].join(" "),
|
|
116
|
+
useWhen: "Per-variation quantity inside a WCart group.",
|
|
56
117
|
keySpecials: {
|
|
57
|
-
field_name: "REQUIRED
|
|
58
|
-
ignoreOnHidden: "boolean —
|
|
118
|
+
field_name: "REQUIRED for the renderer. WCart group path: any unique value (seed's cart_quantity_<id> is fine). Form quantity-LINK path: must be exactly 'quantity' + field_type='number' + linkType/prodId/variationId.",
|
|
119
|
+
ignoreOnHidden: "boolean — DEAD CODE on live (dispatcher does not handle cart-quantity). Has no effect on quantity contribution when hidden.",
|
|
59
120
|
},
|
|
60
121
|
// NOT a FIELD_TYPE (field flag omitted) but the renderer still requires a
|
|
61
122
|
// field_name, so the seed sets one explicitly.
|
|
@@ -77,13 +138,34 @@ export const COMMERCE = [
|
|
|
77
138
|
},
|
|
78
139
|
{
|
|
79
140
|
type: "table", category: "commerce", container: false, defaultName: "Table",
|
|
80
|
-
summary:
|
|
81
|
-
|
|
141
|
+
summary: [
|
|
142
|
+
"Data table rendered server-side from specials.sourceTable (escaped HTML string).",
|
|
143
|
+
"The published SSR renders ONLY unescapeHTML(specials.sourceTable) — the Google Sheets branch is commented out in the publisher (index.js:1172-1226).",
|
|
144
|
+
"EDITOR ≠ LIVE: the editor is DATASET-driven (specials.datasetId → /api/datasets/records) and snapshots its rendered DOM back into specials.sourceTable on every save — an MCP-authored google_sheet_data-only table shows BLANK in the editor, and re-saving from the editor can WIPE sourceTable.",
|
|
145
|
+
"ALWAYS author specials.sourceTable (the escaped HTML string) as the primary content key.",
|
|
146
|
+
"dataType=1 + google_sheet_data: renders blank on first paint / no-JS / SEO (render_v4 Table class overwrites .table-wrapper client-side at table.js:16,81). A google_sheet_data with exactly 1 row renders an empty box (table.js:20-21).",
|
|
147
|
+
].join(" "),
|
|
148
|
+
useWhen: "Pricing/comparison/spec tables. Always provide specials.sourceTable (escaped HTML); google_sheet_data alone is blank on SSR/SEO.",
|
|
82
149
|
keySpecials: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
// --- content (primary) ---
|
|
151
|
+
sourceTable: "PRIMARY CONTENT — escaped HTML string of the full rendered table. The SSR publisher outputs unescapeHTML(sourceTable) directly. Without this the published table is blank. Author this first; the editor will update it from its dataset on re-save.",
|
|
152
|
+
dataType: "0 | 1 — set to 1 to enable the client-side Table renderer; 0 = no client-side render. Does not affect SSR (sourceTable is always used server-side).",
|
|
153
|
+
source: "string — data source label (metadata only; not rendered).",
|
|
154
|
+
sheetID: "string — Google Sheet document id (metadata only; not rendered on publish).",
|
|
155
|
+
google_sheet_data: "string[][] — 2D table data for the client-side renderer (render_v4); row 0 = headers as 'Title|type' (type ∈ image|video|link|time; absent = plain text); rows 1+ are data cells. BLANK on SSR/no-JS — always pair with sourceTable.",
|
|
156
|
+
// --- styling specials (all consumed by exportCss.js:2047-2106 / cssTable) ---
|
|
157
|
+
// These are specials, NOT styles. Values are CSS strings.
|
|
158
|
+
hidden_title: "truthy — hides thead on live (editor only hides when === 'none'). E.g. 'none' to hide on both.",
|
|
159
|
+
head_background: "CSS color string — header row background.",
|
|
160
|
+
head_color: "CSS color string — header row text color.",
|
|
161
|
+
row_even_background: "CSS color string — even data-row background.",
|
|
162
|
+
row_even_color: "CSS color string — even data-row text color.",
|
|
163
|
+
row_odd_background: "CSS color string — odd data-row background.",
|
|
164
|
+
row_odd_color: "CSS color string — odd data-row text color.",
|
|
165
|
+
padding_content: "CSS padding string — cell padding (e.g. '8px 12px').",
|
|
166
|
+
borderWidth: "CSS length string — table border width (e.g. '1px').",
|
|
167
|
+
borderColor: "CSS color string — table border color.",
|
|
168
|
+
borderStyle: "CSS border-style string — table border style (e.g. 'solid').",
|
|
87
169
|
},
|
|
88
170
|
seed: (el) => {
|
|
89
171
|
seedPosition(el);
|
|
@@ -3,7 +3,7 @@ export const CONTENT = [
|
|
|
3
3
|
{
|
|
4
4
|
type: "text-block", category: "content", container: false, defaultName: "Text",
|
|
5
5
|
summary: "Text. specials.text holds the content (may contain inline HTML); specials.tag sets the semantic tag. Supports template variables ({{key}}), formula mode, URL-param injection, and date formatting.",
|
|
6
|
-
useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible.",
|
|
6
|
+
useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible. styles.background on a text-block = a GRADIENT TEXT FILL (emits -webkit-text-fill-color:transparent); you must also set styles['-webkitBackgroundClip']:'text' or the glyphs go invisible. The box background key is styles.backgroundTxt — use that for a colored box behind the text. NEVER set styles.background expecting a box fill. text-block does NOT emit border-radius — for a rounded pill/badge, put a rectangle (borderRadius '13px', pill bg color) BEHIND the text-block (zIndex 2 on the text-block); the rounded shape comes from the rectangle, never from the text-block itself.",
|
|
7
7
|
keySpecials: {
|
|
8
8
|
text: "string — the visible text; may include inline HTML (<b>, <br>, <span style>…). Also supports template variables: {{today}}, {{yesterday}}, {{tomorrow}} (formatted dates), {{coupon_text}}, {{coupon_code}}, {{coupon_codes}}, {{spin_turn_left}}, {{cart_total_price}}, {{cart_subtotal}}, {{cart_shipping_fee}}, {{cart_discount_code}}, {{voucher_price_cart}}, {{cart_item}}, {{cart_bonus_item}}, {{form_error_log}}, {{total_cart}}. Dynamic form field binding: {{formId__fieldName}} substitutes a field value from a sibling form.",
|
|
9
9
|
tag: "p | h1 | h2 | h3 | h4 | h5 | h6 | span | div.",
|
|
@@ -15,6 +15,9 @@ export const CONTENT = [
|
|
|
15
15
|
isFormat: "boolean — apply a date format to template date variables.",
|
|
16
16
|
format: "string — dayjs format string (e.g. 'D/MM/YYYY') used when isFormat=true.",
|
|
17
17
|
formParamSeparator: "string — separator between items when rendering {{formId__form_items}} lists.",
|
|
18
|
+
backgroundTxt: "(styles key) box background color rgba(...) — use this for a colored box behind the text. Do NOT use styles.background for a box fill; that key activates gradient text-fill mode.",
|
|
19
|
+
"-webkitBackgroundClip": "(styles key) set to 'text' when styles.background is a gradient — without it the gradient text-fill makes glyphs invisible.",
|
|
20
|
+
virtualHeight: "(config, per breakpoint, px) — live overrides height:auto with this value. If set it must be ≥ the rendered text height or live clips the text. Optional.",
|
|
18
21
|
},
|
|
19
22
|
seed: (el) => {
|
|
20
23
|
seedPosition(el);
|
|
@@ -35,11 +38,17 @@ export const CONTENT = [
|
|
|
35
38
|
},
|
|
36
39
|
{
|
|
37
40
|
type: "list-paragraph", category: "content", container: false, defaultName: "ListParagraph",
|
|
38
|
-
summary: "Bulleted list. specials.text is a string of <li>…</li> items.",
|
|
39
|
-
useWhen: "Feature checklists, benefit lists. One <li> per bullet.",
|
|
41
|
+
summary: "Bulleted list. specials.text is a string of <li>…</li> items. specials.text is REQUIRED — if omitted the live renderer renders the literal string 'undefined'.",
|
|
42
|
+
useWhen: "Feature checklists, benefit lists. One <li> per bullet. Always set iconSize and linePaddingLeft together — the live renderer defaults are iconSize=40 (very large) and linePaddingLeft=0 (text overlaps bullet). A safe starting config: iconSize:12, iconTop:5, linePaddingLeft:23.",
|
|
40
43
|
keySpecials: {
|
|
41
|
-
text: "string of <li>item</li><li>item</li>… (no <ul> wrapper).
|
|
42
|
-
|
|
44
|
+
text: "string of <li>item</li><li>item</li>… (no <ul> wrapper). REQUIRED — missing text renders the literal string 'undefined' on the live page.",
|
|
45
|
+
iconType: "(config, per breakpoint) 'shape' | 'image' | 'disc' | 'circle' | 'square' | 'decimal' | 'none'… — 'shape' uses an SVG mask colored by iconColor, 'image' uses a background image URL. Live default: 'shape'.",
|
|
46
|
+
iconImage: "(config, per breakpoint) SVG string (must start with '<svg', no leading whitespace — leading whitespace triggers a URL fetch) or image URL when iconType='image'.",
|
|
47
|
+
iconColor: "(config, per breakpoint) rgba(...) — bullet color when iconType='shape'. Live default: black.",
|
|
48
|
+
iconSize: "(config, per breakpoint) bullet icon size in px. Live default: 40 (very large) — always set this explicitly, e.g. 12.",
|
|
49
|
+
iconTop: "(config, per breakpoint) vertical offset for the bullet icon in px. Live default: 0. Use ~4–6 to vertically align with text.",
|
|
50
|
+
linePaddingLeft: "(config, per breakpoint) text indent in px. Live default: 0 (text overlaps bullet) — always set this explicitly to ≥ iconSize+4, e.g. 23.",
|
|
51
|
+
linePaddingBottom: "(config, per breakpoint) line spacing in px.",
|
|
43
52
|
},
|
|
44
53
|
seed: (el) => {
|
|
45
54
|
setBox(el, 400);
|
|
@@ -50,13 +59,17 @@ export const CONTENT = [
|
|
|
50
59
|
},
|
|
51
60
|
{
|
|
52
61
|
type: "image-block", category: "content", container: false, defaultName: "Image Block",
|
|
53
|
-
summary: "Image. The
|
|
54
|
-
useWhen: "Add images where a landing page would have them: hero/product shot, feature icons, about photo, logos.
|
|
62
|
+
summary: "Image. The LIVE published page paints the image from styles.background (exportCss.js) — the server automatically derives this from specials.src on every expand (create/update/validate). specials.src is the EDITOR key; always set it and let the server keep both in sync. config.overlay tints the image.",
|
|
63
|
+
useWhen: "Add images where a landing page would have them: hero/product shot, feature icons, about photo, logos. Set specials.src to the image URL. Use https://placehold.co/WxH?text=Label if you don't have a real image. NEVER leave src empty — it renders blank on the live page.",
|
|
55
64
|
keySpecials: {
|
|
56
|
-
src: "image URL — REQUIRED. Use https://placehold.co/WxH
|
|
57
|
-
|
|
65
|
+
src: "image URL — REQUIRED. The editor reads this key; the server derives styles.background (the live renderer's key) automatically from it: 'center center/ cover no-repeat scroll content-box url(<src>) border-box'. If you hand-write styles.background it must contain url(...). Use https://placehold.co/WxH if no real image.",
|
|
66
|
+
keep_solution: "boolean — true = no-crop CDN resize (preserves aspect ratio); false = crop to box. Live reads config.keep_solution ?? specials.keep_solution. The editor's 'resize' key is editor-only sugar for this — set keep_solution directly.",
|
|
67
|
+
widthBgImage: "(config) CDN width for the background image crop — defaults to the element's styles.width.",
|
|
68
|
+
heightBgImage: "(config) CDN height for the background image crop — defaults to the element's styles.height.",
|
|
69
|
+
topBgImage: "(config) vertical crop offset in px — default 0.",
|
|
70
|
+
leftBgImage: "(config) horizontal crop offset in px — default 0.",
|
|
58
71
|
enable_background_compare: "boolean — show a before/after image-comparison slider (companion config.backgroundCompare holds the second image).",
|
|
59
|
-
overlay: "(config) overlay color rgba(...).",
|
|
72
|
+
overlay: "(config) overlay tint color rgba(...). Gradient border recipe: set styles.borderColor to linear-gradient/radial-gradient AND styles.borderImage to activate a gradient-border underlay.",
|
|
60
73
|
},
|
|
61
74
|
seed: (el) => {
|
|
62
75
|
seedPosition(el);
|
|
@@ -64,39 +77,77 @@ export const CONTENT = [
|
|
|
64
77
|
setStyle(el, "position", "absolute");
|
|
65
78
|
el.specials.imageCompression = true;
|
|
66
79
|
el.specials.src = imgPlaceholder(600, 400);
|
|
80
|
+
// Stamp styles.background from the placeholder src so the live renderer
|
|
81
|
+
// paints the image even before the normalization pass (seed is the baseline).
|
|
82
|
+
const bg = `center center/ cover no-repeat scroll content-box url(${imgPlaceholder(600, 400)}) border-box`;
|
|
83
|
+
el.responsive.desktop.styles.background = bg;
|
|
84
|
+
el.responsive.mobile.styles.background = bg;
|
|
67
85
|
},
|
|
68
86
|
example: {
|
|
69
87
|
id: "hero_img", type: "image-block",
|
|
70
88
|
responsive: {
|
|
71
|
-
desktop: {
|
|
72
|
-
|
|
89
|
+
desktop: {
|
|
90
|
+
styles: {
|
|
91
|
+
top: 40, left: 540, width: 360, height: 300, position: "absolute",
|
|
92
|
+
background: "center center/ cover no-repeat scroll content-box url(https://placehold.co/360x300?text=Product) border-box",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
mobile: {
|
|
96
|
+
styles: {
|
|
97
|
+
top: 260, left: 60, width: 300, height: 240, position: "absolute",
|
|
98
|
+
background: "center center/ cover no-repeat scroll content-box url(https://placehold.co/300x240?text=Product) border-box",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
73
101
|
},
|
|
74
102
|
specials: { src: "https://placehold.co/360x300?text=Product", imageCompression: true },
|
|
75
103
|
},
|
|
76
104
|
},
|
|
77
105
|
{
|
|
78
106
|
type: "rectangle", category: "content", container: false, defaultName: "Rectangle",
|
|
79
|
-
summary: "Colored block — divider, badge background, color band, card backdrop.",
|
|
80
|
-
useWhen: "Backgrounds behind text/groups, dividers, decorative shapes. Style via background/borderRadius/boxShadow.",
|
|
81
|
-
keySpecials: {
|
|
107
|
+
summary: "Colored block — divider, badge background, color band, card backdrop. Also doubles as an SVG icon/shape via per-breakpoint config.svgMask: set a raw <svg> string there and the renderer applies it as a mask; the shape color comes entirely from styles.background. Can also hold a CDN-resized image as a clipped background (styles.background with url(...)). Gradient border: set styles.borderColor to a linear/radial-gradient AND styles.borderImage — the renderer activates the gradient-border underlay. config.overlay adds a tint layer on top of a background image.",
|
|
108
|
+
useWhen: "Backgrounds behind text/groups, dividers, decorative shapes. Style via background/borderRadius/boxShadow. LEAF — it must NEVER have children (validation blocks it): for a card, wrap a group around it and make this the group's full-size FIRST child as the backdrop. For feature/benefit icons, USE THIS with config.svgMask + a brand-colored styles.background INSTEAD of emoji characters in text — it looks far more professional and scales cleanly on all devices.",
|
|
109
|
+
keySpecials: {
|
|
110
|
+
svgMask: "(config, per breakpoint) — raw <svg viewBox='…'>…</svg> string. The renderer base64-encodes it and applies it as -webkit-mask-image on the element; the shape color comes from styles.background (solid rgba or gradient). The SVG's own fill/stroke colors are IGNORED — only painted pixels (alpha) matter, so the SVG must paint via fill or stroke to produce a silhouette. Aspect: the published renderer force-inserts preserveAspectRatio='none' (SVG stretches to the box) but the editor preview keeps the SVG's own aspect ratio — so ALWAYS match the box aspect to the viewBox aspect (square box for a square viewBox) or editor and live render differently. Use single quotes for SVG attributes to avoid JSON-escaping noise. Set on BOTH desktop and mobile config or mobile renders a plain rectangle.",
|
|
111
|
+
overlay: "(config) tint layer rgba(...) drawn on top of the background image.",
|
|
112
|
+
},
|
|
82
113
|
seed: (el) => {
|
|
83
114
|
seedPosition(el);
|
|
84
115
|
setBox(el, 100, 100);
|
|
85
116
|
},
|
|
117
|
+
example: {
|
|
118
|
+
id: "icon_star", type: "rectangle",
|
|
119
|
+
responsive: {
|
|
120
|
+
desktop: {
|
|
121
|
+
styles: { top: 40, left: 456, width: 48, height: 48, background: "rgba(34,197,94,1)" },
|
|
122
|
+
config: { svgMask: "<svg viewBox='0 0 24 24'><path fill='black' d='M12 2l2.9 6.26L22 9.27l-5 4.87L18.18 21 12 17.27 5.82 21 7 14.14l-5-4.87 7.1-1.01z'/></svg>" },
|
|
123
|
+
},
|
|
124
|
+
mobile: {
|
|
125
|
+
styles: { top: 40, left: 186, width: 48, height: 48, background: "rgba(34,197,94,1)" },
|
|
126
|
+
config: { svgMask: "<svg viewBox='0 0 24 24'><path fill='black' d='M12 2l2.9 6.26L22 9.27l-5 4.87L18.18 21 12 17.27 5.82 21 7 14.14l-5-4.87 7.1-1.01z'/></svg>" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
specials: {},
|
|
130
|
+
},
|
|
86
131
|
},
|
|
87
132
|
{
|
|
88
133
|
type: "line", category: "content", container: false, defaultName: "Line",
|
|
89
|
-
summary: "Horizontal rule / divider line.",
|
|
90
|
-
useWhen: "Separating content rows.",
|
|
134
|
+
summary: "Horizontal rule / divider line. The visible line is the TOP border only: the base CSS zeroes left/right/bottom borders. Thickness is styles.borderWidth (NOT styles.height — height is auto). The .line-css class adds padding:8px 0 so the line sits 8px below the element's top. Color via styles.borderColor; style via styles.borderStyle.",
|
|
135
|
+
useWhen: "Separating content rows. Always set borderWidth, borderStyle, and borderColor — without them the line renders as an invisible element.",
|
|
91
136
|
keySpecials: {},
|
|
92
137
|
seed: (el) => {
|
|
93
138
|
setBox(el, 236);
|
|
139
|
+
el.responsive.desktop.styles.borderWidth = 1;
|
|
140
|
+
el.responsive.desktop.styles.borderStyle = "solid";
|
|
141
|
+
el.responsive.desktop.styles.borderColor = "rgba(208,213,221,1)";
|
|
142
|
+
el.responsive.mobile.styles.borderWidth = 1;
|
|
143
|
+
el.responsive.mobile.styles.borderStyle = "solid";
|
|
144
|
+
el.responsive.mobile.styles.borderColor = "rgba(208,213,221,1)";
|
|
94
145
|
},
|
|
95
146
|
},
|
|
96
147
|
{
|
|
97
148
|
type: "button", category: "content", container: false, defaultName: "Button",
|
|
98
149
|
summary: "Clickable button. Label in specials.text; behavior in the events array. Supports the same template variable system as text-block ({{today}}, {{cart_total_price}}, {{formId__fieldName}}, etc.).",
|
|
99
|
-
useWhen: "Every CTA. Add a click event: open_link (to URL), open_popup (lead modal), scroll_to (anchor). Inside a form, a button submits the form.",
|
|
150
|
+
useWhen: "Every CTA. Add a click event: open_link (to URL), open_popup (lead modal), scroll_to (anchor). Inside a form, a button submits the form. Hover color changes: use the modern change_color action with change_color_type:'text'|'background'|'border'|'reset' — the legacy change_background (--hover-color var) and change_text_color (--hover-text var) actions are broken on published pages because the CSS variables are never defined at publish time.",
|
|
100
151
|
keySpecials: {
|
|
101
152
|
text: "button label — supports template variables (same set as text-block: {{today}}, {{cart_total_price}}, {{formId__fieldName}}, etc.).",
|
|
102
153
|
required: "boolean — gate by form validity.",
|
|
@@ -125,16 +176,17 @@ export const CONTENT = [
|
|
|
125
176
|
},
|
|
126
177
|
{
|
|
127
178
|
type: "video", category: "content", container: false, defaultName: "Video",
|
|
128
|
-
summary: "Video player (YouTube/Vimeo/upload/CDN).
|
|
129
|
-
useWhen: "Demo or promo videos. Set specials.img to a poster placeholder (https://placehold.co/640x360) when there's no real thumbnail.",
|
|
179
|
+
summary: "Video player (YouTube/Vimeo/upload/CDN). Required specials by typeVideo: 'youtube' → specials.id REQUIRED (YouTube videoId); 'vimeo' → specials.video REQUIRED (full Vimeo URL); 'webcake' → specials.video REQUIRED (missing causes a video.replace TypeError that breaks the page); 'upload'/other → specials.video (src = video_cdn || video). Poster: first url() in styles.background takes precedence over specials.img — a flat background color (no url()) suppresses the img poster. specials.videoFit default 'cover'; false = 'contain'. showControl default-off hides Vimeo controls entirely.",
|
|
180
|
+
useWhen: "Demo or promo videos. Set specials.img to a poster placeholder (https://placehold.co/640x360) when there's no real thumbnail. Do NOT set a flat styles.background color — it suppresses the poster image.",
|
|
130
181
|
keySpecials: {
|
|
131
|
-
typeVideo: "'youtube' | 'vimeo' | 'webcake' | 'upload' — video source type.",
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
video_cdn: "string — CDN video URL or identifier.",
|
|
135
|
-
img: "string — poster/thumbnail image URL
|
|
182
|
+
typeVideo: "'youtube' | 'vimeo' | 'webcake' | 'upload' — video source type. REQUIRED to match the right specials keys below.",
|
|
183
|
+
id: "string — YouTube video ID. REQUIRED when typeVideo='youtube'. Example: 'dQw4w9WgXcQ'.",
|
|
184
|
+
video: "string — full video URL. REQUIRED when typeVideo='vimeo' (full Vimeo page URL, e.g. https://vimeo.com/123456) or typeVideo='webcake' (missing crashes the page with TypeError). For 'upload' / other CDN types this is the raw video file URL.",
|
|
185
|
+
video_cdn: "string — CDN video URL or identifier (for 'upload'/'webcake' types; renderer uses video_cdn ?? video as source).",
|
|
186
|
+
img: "string — poster/thumbnail image URL. Use a placeholder if none: https://placehold.co/640x360. Overridden by the first url() in styles.background — do NOT set a flat styles.background color or the poster is suppressed.",
|
|
187
|
+
videoFit: "string — 'cover' (default, fills box) | false (contain, shows letterbox).",
|
|
136
188
|
autoReplay: "boolean — loop the video.",
|
|
137
|
-
showControl: "boolean — show player controls.",
|
|
189
|
+
showControl: "boolean — show player controls (default off; when off, Vimeo controls are entirely hidden).",
|
|
138
190
|
hideRelated: "boolean — hide YouTube related videos at end.",
|
|
139
191
|
muteOnPlay: "boolean — mute audio when video plays.",
|
|
140
192
|
autoPlay: "boolean — autoplay on load.",
|
|
@@ -148,18 +200,20 @@ export const CONTENT = [
|
|
|
148
200
|
},
|
|
149
201
|
{
|
|
150
202
|
type: "gallery", category: "content", container: false, defaultName: "Gallery",
|
|
151
|
-
summary: "Multi-image/video gallery with thumbnail strip. Content comes entirely from specials.media — this is NOT a container and has no children.",
|
|
152
|
-
useWhen: "Photo grids/galleries with several images or videos. No image API — fill specials.media with placeholder image objects (see media). NEVER use plain URL strings — the gallery reads item.link and renders blank for a string.",
|
|
203
|
+
summary: "Multi-image/video gallery with thumbnail strip. Content comes entirely from specials.media — this is NOT a container and has no children. config.showThumbnail is tri-state: when UNSET the editor hides thumbnails but the live renderer shows an 80px strip overlapping the main image — always set it explicitly.",
|
|
204
|
+
useWhen: "Photo grids/galleries with several images or videos. No image API — fill specials.media with placeholder image objects (see media). NEVER use plain URL strings — the gallery reads item.link and renders blank for a string. Video items: use typeVideo 'youtube' or 'webcake' (NOT 'upload' — renders an empty item); set item.type:'video' so the play-overlay and zoom/lightbox fire correctly.",
|
|
153
205
|
keySpecials: {
|
|
154
|
-
media: "array of media OBJECTS (NOT plain URLs — the gallery reads item.link). Image item: {type:'image', link:'<url>', linkVideo:'', typeVideo:'youtube', imageCompression:true}
|
|
206
|
+
media: "array of media OBJECTS (NOT plain URLs — the gallery reads item.link). Image item: {type:'image', link:'<url>', linkVideo:'', typeVideo:'youtube', imageCompression:true}. Video item: {type:'video', link:'<poster-url>', linkVideo:'<video-url>', typeVideo:'youtube'|'webcake', imageCompression:true} — item.type must be 'video' for the play-overlay and lightbox to fire; typeVideo 'upload' renders an empty item on live.",
|
|
207
|
+
showThumbnail: "(config) boolean — show the thumbnail strip. ALWAYS set explicitly: when unset the live renderer shows an 80px strip overlapping the main image while the editor hides it. Default when set: true (thumbnailWidth/Height:80, distanceAmong:10, position:bottom, distanceToGallery:10).",
|
|
155
208
|
allowZoom: "'off' | 'carousel' | 'lightbox' — (config) zoom/lightbox mode when clicking an image.",
|
|
156
209
|
showNavigation: "boolean — (config) show prev/next navigation arrows.",
|
|
157
|
-
thumbnailAutoplay: "number (ms) | 'off' — (config) auto-advance thumbnails
|
|
158
|
-
thumbnailAutoplayRepeat: "boolean — (config) loop thumbnail autoplay.",
|
|
210
|
+
thumbnailAutoplay: "number (ms) | 'off' — (config) auto-advance thumbnails; delay default 3000ms. Starts only when thumbnailAutoplay != 'off' AND thumbnailAutoplayRepeat is truthy.",
|
|
211
|
+
thumbnailAutoplayRepeat: "boolean — (config) loop thumbnail autoplay. Required alongside thumbnailAutoplay for autoplay to start.",
|
|
159
212
|
thumbnailPosition: "'top' | 'bottom' | 'left' | 'right' — (config) position of the thumbnail strip relative to the main image.",
|
|
160
|
-
thumbnailWidth: "number — (config) width of each thumbnail in px.",
|
|
161
|
-
thumbnailHeight: "number — (config) height of each thumbnail in px.",
|
|
162
|
-
distanceAmong: "number — (config) gap between thumbnails in px.",
|
|
213
|
+
thumbnailWidth: "number — (config) width of each thumbnail in px. Default 80.",
|
|
214
|
+
thumbnailHeight: "number — (config) height of each thumbnail in px. Default 80.",
|
|
215
|
+
distanceAmong: "number — (config) gap between thumbnails in px. Default 10.",
|
|
216
|
+
distanceToGallery: "number — (config) gap between thumbnail strip and main image in px. Default 10.",
|
|
163
217
|
},
|
|
164
218
|
seed: (el) => {
|
|
165
219
|
seedPosition(el);
|
|
@@ -173,14 +227,18 @@ export const CONTENT = [
|
|
|
173
227
|
typeVideo: "youtube",
|
|
174
228
|
imageCompression: true,
|
|
175
229
|
}));
|
|
230
|
+
// Always set showThumbnail explicitly — unset causes the live renderer to
|
|
231
|
+
// overlay an 80px strip on the main image even though the editor hides it.
|
|
232
|
+
el.responsive.desktop.config = { ...el.responsive.desktop.config, showThumbnail: true };
|
|
233
|
+
el.responsive.mobile.config = { ...el.responsive.mobile.config, showThumbnail: true };
|
|
176
234
|
// gallery has NO children — content comes entirely from specials.media (gallery.js never reads vm.children)
|
|
177
235
|
},
|
|
178
236
|
},
|
|
179
237
|
{
|
|
180
238
|
type: "html-box", category: "content", container: false, defaultName: "HTML Box",
|
|
181
|
-
summary: "Raw HTML embed. specials.html holds the markup (
|
|
239
|
+
summary: "Raw HTML embed. specials.html holds the markup stored HTML-escaped (unescaped at render via v-html). Contrast with editor-blog which stores html RAW. Embedded <iframe> is auto-stretched to 100%×100% of the box. Wrapper height is FIXED to styles.height — content taller than the box overflows. in the stored value becomes a space at render. Use unescape-safe HTML (e.g. '<' → the literal character '<' after unescape).",
|
|
182
240
|
useWhen: "Embedding third-party widgets or custom markup the standard elements can't express.",
|
|
183
|
-
keySpecials: { html: "string — raw HTML content (the
|
|
241
|
+
keySpecials: { html: "string — raw HTML content stored HTML-escaped (e.g. '<p>Hello</p>'); the renderer unescapes it before injecting via v-html. The wrapper height is FIXED to styles.height — content that is taller overflows the box. An embedded <iframe> is auto-stretched to 100%×100% of the box." },
|
|
184
242
|
seed: (el) => {
|
|
185
243
|
seedPosition(el);
|
|
186
244
|
setBox(el, 280, 310);
|
|
@@ -189,9 +247,9 @@ export const CONTENT = [
|
|
|
189
247
|
},
|
|
190
248
|
{
|
|
191
249
|
type: "editor-blog", category: "content", container: false, defaultName: "Editor blog",
|
|
192
|
-
summary: "Long-form rich text / article body. specials.html holds the rich-text markup.",
|
|
193
|
-
useWhen: "Blog/article content blocks.",
|
|
194
|
-
keySpecials: { html: "string — rich-text HTML content
|
|
250
|
+
summary: "Long-form rich text / article body. specials.html holds the rich-text markup stored RAW (NOT escaped). Contrast with html-box which stores escaped HTML. The publisher injects specials.html raw with NO unescape — storing escaped HTML causes the live page to display literal '<p>' text instead of rendered markup. Live wrapper height is FIXED to styles.height (the editor shows auto) — a long article overflows the band; set a generous height.",
|
|
251
|
+
useWhen: "Blog/article content blocks. Set styles.height to a generous value (≥ the rendered article height) — content taller than the box overflows the live page silently.",
|
|
252
|
+
keySpecials: { html: "string — rich-text HTML content stored RAW (not escaped). Store exactly what you want the browser to render — e.g. '<p>Hello</p>' not '<p>Hello</p>'. Storing escaped HTML causes the live page to display literal tag strings. The live wrapper height is FIXED to styles.height — set generously." },
|
|
195
253
|
seed: (el) => {
|
|
196
254
|
el.responsive.mobile.styles.width = 340;
|
|
197
255
|
el.responsive.mobile.styles.height = 303;
|