/* style.css — cookbook/2-compact-2col — `.demo-mini-builder--compact-2col`.
*
* Self-contained chrome variant (W6.C.B.2). Pair this file with init.js
* (sibling) and dist/builder.css + dist/builder.js, and you have a fully-
* working Builder mount in 4 files. NO shared mini-builder.css. NO
* examples.css. The `.demo-mini-builder--compact-2col` rules below ARE
* the variant — copy them verbatim into your own stylesheet.
*
* ─── Buyer use case ─────────────────────────────────────────────────
* Read-mostly editor where users edit text + colours via the right-side
* Settings panel only — no widgets palette. Pick this when content is
* AUTHORED elsewhere (CMS export, AI generation, copy-team handoff) and
* the buyer's job is just polish: tweak headlines, swap brand colours,
* fix typos. Saves vertical real-estate by dropping the header strip.
*
* ─── Constructor null-args (W6.A.A.1 null-tolerance) ────────────────
* new Builder({
* mainContainer: '#YourCanvas',
* settingsContainer: '#YourSettings',
* widgetsContainer: null, // ← no widgets palette
* });
* The engine skips the omitted `widgetsContainer` mount cleanly — no
* NPE, no console error, regardless of whether a DOM node exists for
* it. `settingsContainer` IS supplied (single null-arg pattern), so
* the right-hand panel still renders the per-element Settings UI.
*
* ─── Tokens consumed (already exposed by dist/builder.css) ──────────
* --bjs-bg, --bjs-bg-subtle, --bjs-border, --bjs-border-light,
* --bjs-radius-lg, --bjs-radius-md, --bjs-text, --bjs-text-secondary,
* --bjs-fs-sm, --bjs-fs-base, --bjs-space-2, --bjs-space-3,
* --bjs-primary.
*/
/* ─── Base wrapper (4-slot grid; canvas-only fallback) ─────────────── */
.demo-mini-builder {
display: grid;
width: 100%;
height: 480px;
border: 1px solid var(--bjs-border);
border-radius: var(--bjs-radius-lg);
background: var(--bjs-bg);
color: var(--bjs-text);
font-size: var(--bjs-fs-base);
overflow: hidden;
box-sizing: border-box;
grid-template-areas: "canvas";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.demo-mini-builder *,
.demo-mini-builder *::before,
.demo-mini-builder *::after { box-sizing: border-box; }
.demo-mini-builder__canvas,
.demo-mini-builder__settings {
overflow: auto;
min-height: 0;
min-width: 0;
}
.demo-mini-builder__canvas {
grid-area: canvas;
background: var(--bjs-bg);
position: relative;
}
.demo-mini-builder__settings {
grid-area: settings;
background: var(--bjs-bg-subtle);
border-left: 1px solid var(--bjs-border-light);
padding: var(--bjs-space-2);
}
/* ─── Variant: --compact-2col ──── canvas + right Settings ──────────── */
.demo-mini-builder--compact-2col {
grid-template-areas: "canvas settings";
grid-template-columns: 1fr 240px;
}
/* ─── Host toolbar ───────────────────────────────────────────────────
* The `--compact-2col` variant has NO header slot — the wrapper is just
* canvas + settings. So the buyer's Save button (and any other host
* chrome: title, breadcrumb, mode toggle) lives OUTSIDE the wrapper, in
* the host page's own toolbar. The class below is a tiny convention so
* the cookbook's example renders a sensible bar without leaning on
* `examples.css`. Buyers can drop this rule and use their host app's
* existing header / nav / tab bar instead — the Builder mount doesn't
* care where the click handler lives.
*/
.demo-host-toolbar {
display: flex;
align-items: center;
gap: var(--bjs-space-2);
margin-bottom: var(--bjs-space-3);
font-size: var(--bjs-fs-sm);
color: var(--bjs-text-secondary);
}
.demo-host-toolbar__title {
margin-right: auto;
font-weight: 600;
color: var(--bjs-text);
font-size: var(--bjs-fs-base);
}
.demo-host-toolbar button {
appearance: none;
-webkit-appearance: none;
border: 1px solid var(--bjs-border-light);
background: var(--bjs-bg);
color: var(--bjs-text);
padding: 4px 10px;
border-radius: var(--bjs-radius-md);
font: inherit;
font-size: var(--bjs-fs-sm);
line-height: 1.2;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.demo-host-toolbar button:hover {
background: var(--bjs-bg-subtle);
border-color: var(--bjs-border);
}
.demo-host-toolbar button:focus-visible {
outline: 2px solid var(--bjs-primary);
outline-offset: 2px;
}
.demo-host-toolbar button:active {
transform: translateY(1px);
}
.demo-host-toolbar button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
@media (prefers-reduced-motion: reduce) {
.demo-mini-builder, .demo-mini-builder *,
.demo-host-toolbar button { transition: none; }
}
Compact 2-col — canvas + right Settings
Read-mostly editor: text and colours come from the right-side Settings panel; no widgets palette. Pick this when content is authored elsewhere (CMS, AI, copy team) and the buyer's job is just polish.
Walkthrough
-
Wrap two slots — canvas + settings, with a host toolbar above
The
--compact-2colvariant has no header slot — only__canvasand__settings. Any host chrome you need (title, Save button, mode toggle) lives OUTSIDE the wrapper, in your own toolbar. Both slots must carry anid;init.jswires Builder to the canvas slot and renders the Settings panel into the settings slot.HTML<div class="demo-host-toolbar"> <span class="demo-host-toolbar__title">My builder</span> <button type="button" id="CookbookCompact2colSave">Save</button> </div> <div class="demo-mini-builder demo-mini-builder--compact-2col"> <div class="demo-mini-builder__canvas" id="CookbookCompact2colCanvas"></div> <div class="demo-mini-builder__settings" id="CookbookCompact2colSettings"></div> </div> -
Mount with
widgetsContainer: nullonlyW6.A.A.1 null-tolerance — passing
nullforwidgetsContainermakes the engine skip the widget palette mount cleanly.settingsContainerIS supplied (single null-arg pattern), so the right-hand panel still renders the per-element Settings UI when a buyer clicks an element on the canvas.JavaScriptvar builder = new Builder({ mainContainer: '#CookbookCompact2colCanvas', widgetsContainer: null, // <- no widgets palette settingsContainer: '#CookbookCompact2colSettings', historyUI: false }); builder.load( window.THEME_JSON, window.THEME_TEMPLATES, window.THEME_CONFIG_DATA, window.MEDIA_URL ); -
Wire the host toolbar's Save button —
getHtml()+getData()The host toolbar lives outside the wrapper, so its Save button is reached the same way any other DOM element is —
document.getElementById. The example below pops analert()with the round-trip output; production hosts swap the alert for afetch()POST to their backend.getHtml()returns the rendered page (string);getData()returns the page tree (object).JavaScriptdocument.getElementById('CookbookCompact2colSave').addEventListener('click', function () { var html = builder.getHtml(); var data = builder.getData(); alert( 'Saved · ' + html.length + ' chars HTML, ' + Object.keys(data).length + ' top-level page-tree keys.' ); });
The whole snippet
Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.
// init.js — cookbook/2-compact-2col.
//
// Mounts a Builder instance into the `.demo-mini-builder--compact-2col`
// scaffold rendered alongside this file. Reads four named globals
// (BUILDER.md RULE I split-globals convention):
//
// window.THEME_JSON — page tree (the sample JSON)
// window.THEME_TEMPLATES — { templateKey: HTML EJS string }
// window.THEME_CONFIG_DATA — theme's index.json verbatim
// window.MEDIA_URL — base URL for relative asset paths
//
// The host page emits these (server-rendered via PHP `ThemeRegistry::
// resolveBundle()` in this demo; production hosts can hardcode them or
// pre-render from any backend). The engine consumes them in the same
// order as the named globals — no wrapper object.
//
// W6.A.A.1 null-tolerance — passing `null` for `widgetsContainer` makes
// the engine skip that sub-system mount cleanly. No NPE, no console
// error, regardless of whether the corresponding DOM node exists. That
// contract is what makes `--compact-2col` chrome possible — only the
// canvas + settings slots are in the buyer's HTML.
(function () {
'use strict';
var canvas = document.getElementById('CookbookCompact2colCanvas');
var settings = document.getElementById('CookbookCompact2colSettings');
if (!canvas || !settings || typeof window.Builder !== 'function') return;
var builder = new window.Builder({
mainContainer: '#CookbookCompact2colCanvas',
widgetsContainer: null, // ← no widgets palette
settingsContainer: '#CookbookCompact2colSettings',
historyUI: false
});
builder.load(
window.THEME_JSON,
window.THEME_TEMPLATES,
window.THEME_CONFIG_DATA,
window.MEDIA_URL,
function () {
// Wire the host toolbar's Save button. Production host
// replaces the alert with a fetch() POST to its own backend.
// getHtml() returns the rendered email/page (string);
// getData() returns the page tree (object) for round-trip
// reload.
var saveBtn = document.getElementById('CookbookCompact2colSave');
if (saveBtn) {
saveBtn.addEventListener('click', function () {
var html = builder.getHtml();
var data = builder.getData();
alert(
'Saved · ' + html.length + ' chars HTML, ' +
Object.keys(data).length + ' top-level page-tree keys.'
);
});
}
// Expose for buyer-side debugging + spec assertions.
window.cookbookCompact2colBuilder = builder;
}
);
})();
Notes
Why one null argument? Builder.js's constructor accepts widgetsContainer + settingsContainer as optional. Passing null for widgetsContainer tells the engine: "don't mount the widget palette." Internally (W6.A.A.1 null-tolerance, src/includes/Builder.js) the engine guards every reach into that sub-system with a presence check. Selection, history, save, getHtml, plus the per-element Settings UI on the right all work identically to the full 3-column chrome.
Why no header slot? The variant trades the header for vertical canvas space. Since users in this preset are doing copy + colour edits (not adding new blocks), a tall canvas matters more than a Save / Undo bar. The host page's existing toolbar / nav / breadcrumb supplies the chrome — see Step 1's .demo-host-toolbar. Buyers who DO want a header strip should pick 1-minimal (header + canvas) or 4-rich-3col-header (header + 3-col).
Where do the four window.* globals come from? The host page renders them server-side. In this demo, _partials/example.php calls ThemeRegistry::resolveBundle('default', 'master/sample/email/SimpleText') and emits the four named globals before init.js loads. In your own project, render them from any backend (PHP, Node, Python, Go) — only the four names matter to the engine. See Quickstart for the canonical pattern.
Save flow? The demo above pops an alert() so the round-trip is visible without a backend. Production hosts replace the alert with a fetch(saveUrl, { method: 'POST', body: JSON.stringify({ html, data }) }). Hello World wires the full POST + response toast pattern.