Cookbook · 2 of 8

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

  1. Wrap two slots — canvas + settings, with a host toolbar above

    The --compact-2col variant has no header slot — only __canvas and __settings. Any host chrome you need (title, Save button, mode toggle) lives OUTSIDE the wrapper, in your own toolbar. Both slots must carry an id; init.js wires 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>
  2. Mount with widgetsContainer: null only

    W6.A.A.1 null-tolerance — passing null for widgetsContainer makes the engine skip the widget palette mount cleanly. settingsContainer IS 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.

    JavaScript
    var 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
    );
  3. 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 an alert() with the round-trip output; production hosts swap the alert for a fetch() POST to their backend. getHtml() returns the rendered page (string); getData() returns the page tree (object).

    JavaScript
    document.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.'
        );
    });

Live demo

demo-mini-builder--compact-2col
My builder

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.

2 files · 206 lines · 7.7 KB
/* 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; }
}
// 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.