Cookbook · 3 of 8

Compact 3-col — widgets + canvas + settings

Classic app-chrome at 1024 px+. Drag from the left widget palette, edit on the canvas, polish on the right Settings panel. Tighter than the full demo builder — narrower 200 px / 240 px sidebars leave more canvas room for embed in a denser host app.

Walkthrough

  1. Wrap three slots — widgets · canvas · settings, with a host toolbar above

    The --compact-3col variant has no header slot — three slots inside the wrapper (__widgets · __canvas · __settings) plus your own toolbar above. Each slot needs an id; init.js wires Builder to all three. Same host-toolbar convention as 2-compact-2col — chrome lives outside the wrapper.

    HTML
    <div class="demo-host-toolbar">
        <span class="demo-host-toolbar__title">My builder</span>
        <button type="button" id="CookbookCompact3colSave">Save</button>
    </div>
    <div class="demo-mini-builder demo-mini-builder--compact-3col">
        <div class="demo-mini-builder__widgets"  id="CookbookCompact3colWidgets"></div>
        <div class="demo-mini-builder__canvas"   id="CookbookCompact3colCanvas"></div>
        <div class="demo-mini-builder__settings" id="CookbookCompact3colSettings"></div>
    </div>
  2. Mount all three containers — no null args

    Every sub-system is wired to a real DOM id. The widget palette renders on the left, the canvas in the middle, the per-element Settings panel on the right. This is the most "complete" cookbook variant — the buyer gets the full editing experience without the header-actions row that 4-rich-3col-header adds.

    JavaScript
    var builder = new Builder({
        mainContainer:     '#CookbookCompact3colCanvas',
        widgetsContainer:  '#CookbookCompact3colWidgets',
        settingsContainer: '#CookbookCompact3colSettings',
        historyUI:         false
    });
    
    builder.load(
        window.THEME_JSON,
        window.THEME_TEMPLATES,
        window.THEME_CONFIG_DATA,
        window.MEDIA_URL,
        function () { /* see Step 3 — register widgets here */ }
    );
  3. Populate the widget palette — pick which widgets ship

    The engine renders the wrapper but does NOT auto-populate the palette — buyers pick which widgets appear and how they're grouped. Inside the load() callback, call builder.widgetsBox.addWidget(new W(), { group: '…' }) for each widget you want, then render(). Every built-in widget ships as a top-level window.<Name>Widget global; for a curated list, drop names from the array; for buyer-supplied widgets, append your own classes.

    JavaScript
    function registerWidgets(b) {
        var basic = [
            'ParagraphWidget', 'HeadingWidget', 'DividerWidget',
            'ImageWidget',     'ButtonWidget',  'GridWidget',
        ];
        basic.forEach(function (name) {
            var W = window[name];
            if (typeof W === 'function') {
                b.widgetsBox.addWidget(new W(), { group: 'Basic' });
            }
        });
    
        var imageText = [
            'ImageTextLeftWidget', 'ImageTextRightWidget', 'ImageTextTopWidget',
        ];
        imageText.forEach(function (name) {
            var W = window[name];
            if (typeof W === 'function') {
                b.widgetsBox.addWidget(new W(), { group: 'Image & Text', type: 'image' });
            }
        });
    
        b.widgetsBox.render();
    }
  4. Wire the host toolbar's Save button — getHtml() + getData()

    Same pattern as 2-compact-2col: the 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.

    JavaScript
    document.getElementById('CookbookCompact3colSave').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-3col
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 · 256 lines · 9.6 KB
/* style.css — cookbook/3-compact-3col — `.demo-mini-builder--compact-3col`.
 *
 * Self-contained chrome variant (W6.C.B.3). 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-3col` rules below ARE
 * the variant — copy them verbatim into your own stylesheet.
 *
 * ─── Buyer use case ─────────────────────────────────────────────────
 * Classic app-chrome at 1024 px+. All three sub-systems wired: drag from
 * the left widget palette, edit on the canvas, polish on the right
 * Settings panel. Tighter than the full demo builder for embed in a
 * denser app — narrower 200 px / 240 px sidebars leave more canvas room
 * than the default 280 px each.
 *
 * Variant has NO header slot — Save / Undo / Redo live OUTSIDE the
 * wrapper in a host-supplied toolbar (same pattern as `2-compact-2col`).
 * If you need an in-wrapper header, jump to `4-rich-3col-header`.
 *
 * ─── Constructor (no null args) ─────────────────────────────────────
 *   new Builder({
 *       mainContainer:     '#YourCanvas',
 *       widgetsContainer:  '#YourWidgets',
 *       settingsContainer: '#YourSettings',
 *   });
 * All three sub-systems mount cleanly. The W6.A.A.1 null-tolerance
 * contract still applies if you ever drop a sub-system later, but for
 * compact-3col every container is wired to a real DOM id.
 *
 * ─── 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__widgets,
.demo-mini-builder__canvas,
.demo-mini-builder__settings {
    overflow: auto;
    min-height: 0;
    min-width: 0;
}

.demo-mini-builder__widgets {
    grid-area: widgets;
    background: var(--bjs-bg-subtle);
    border-right: 1px solid var(--bjs-border-light);
    padding: var(--bjs-space-2);
}

.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-3col ──── widgets · canvas · settings ──────── */
.demo-mini-builder--compact-3col {
    grid-template-areas: "widgets canvas settings";
    /* Settings track is 280 px so DimensionControl's
     * "label · slider · value · unit" row fits without horizontal
     * scroll. The engine ships a `@container` compact mode at < 220 px
     * (slider hides, label tightens) — kept available as a safety
     * net for buyers who narrow the panel further. */
    grid-template-columns: 200px 1fr 280px;
}

/* ─── Host toolbar ───────────────────────────────────────────────────
 * The `--compact-3col` variant has NO header slot — Save / Undo / Redo
 * live OUTSIDE the wrapper, in the host page's own toolbar. Same
 * convention as `2-compact-2col`. Reusing this rule across header-less
 * variants means buyers learn ONE pattern: "host chrome lives in your
 * own bar; the wrapper is just the editor."
 *
 * If your host app already provides a header / nav bar, drop this rule
 * and wire your existing button to the same handler — the Builder
 * doesn't care where the click comes from.
 */
.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/3-compact-3col.
//
// Mounts a Builder instance into the `.demo-mini-builder--compact-3col`
// 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
//
// All three sub-systems wired (no null args). The widget palette on the
// left is populated explicitly via `builder.widgetsBox.addWidget(...)`
// inside the load() callback — buyers pick which widgets show up + the
// group labels. Save / Undo / Redo live OUTSIDE the wrapper in the host
// toolbar (variant has no header slot).

(function () {
    'use strict';

    var canvas   = document.getElementById('CookbookCompact3colCanvas');
    var widgets  = document.getElementById('CookbookCompact3colWidgets');
    var settings = document.getElementById('CookbookCompact3colSettings');
    if (!canvas || !widgets || !settings || typeof window.Builder !== 'function') return;

    var builder = new window.Builder({
        mainContainer:     '#CookbookCompact3colCanvas',
        widgetsContainer:  '#CookbookCompact3colWidgets',
        settingsContainer: '#CookbookCompact3colSettings',
        historyUI:         false
    });

    builder.load(
        window.THEME_JSON,
        window.THEME_TEMPLATES,
        window.THEME_CONFIG_DATA,
        window.MEDIA_URL,
        function () {
            // Populate the widget palette. The engine ships every widget
            // class as a top-level `window.<Name>Widget` global (auto-
            // injected by `dist/builder.js`); pick the ones you want, set
            // their group label, then call `widgetsBox.render()` to draw
            // the palette tiles. Buyers customise this list by adding
            // their own widget classes (see `cookbook/.../extensions/`
            // examples) or by curating which built-ins ship.
            registerWidgets(builder);

            // 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('CookbookCompact3colSave');
            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.cookbookCompact3colBuilder = builder;
        }
    );

    function registerWidgets(b) {
        // A representative set — enough for an editorial / newsletter
        // canvas. Add or remove freely; group labels become the palette's
        // section headings.
        var basic = [
            'ParagraphWidget', 'HeadingWidget', 'DividerWidget',
            'ImageWidget',     'ButtonWidget',  'GridWidget',
        ];
        basic.forEach(function (name) {
            var W = window[name];
            if (typeof W === 'function') {
                b.widgetsBox.addWidget(new W(), { group: 'Basic' });
            }
        });

        var imageText = [
            'ImageTextLeftWidget', 'ImageTextRightWidget', 'ImageTextTopWidget',
        ];
        imageText.forEach(function (name) {
            var W = window[name];
            if (typeof W === 'function') {
                b.widgetsBox.addWidget(new W(), { group: 'Image & Text', type: 'image' });
            }
        });

        b.widgetsBox.render();
    }
})();

Notes

Why no null arguments? The classic 3-column variant uses every sub-system the engine offers — drag-from-palette, on-canvas editing, per-element Settings. Each constructor option (mainContainer, widgetsContainer, settingsContainer) gets a real DOM id. The W6.A.A.1 null-tolerance contract is still there if you ever drop a sub-system later, but for compact-3col the typical buyer wires all three.

Why doesn't the engine auto-populate the widget palette? Buyers curate which widgets ship — full set for content authors, narrow set for "edit only" hosts, custom widgets via the extensions/1-custom-widget/ recipe. The cookbook's init.js registers a representative set inside the load() callback (Step 3 above). To curate: drop names from the basic / imageText arrays. To extend: b.widgetsBox.addWidget(new MyCustomWidget(), { group: 'My App' }) right alongside the built-ins.

Why the narrower 200 px / 240 px sidebars? The default demo builder (builder.php) ships with 280 px sidebars on each side, comfortable at 1440 px+ viewports. Embedded inside a host app's content area, that often eats too much canvas. --compact-3col drops the widgets palette to 200 px and Settings to 240 px — still legible, leaves more pixels for the editing surface. Buyers with even tighter constraints pick 5-sidebar-only (Settings floats over the canvas).

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/Editorial_Newsletter') 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.

Want a header row of buttons? Pick 4-rich-3col-header — same 3-column layout but with a top header strip wired to Save / Undo / Redo / Export / Preview. That preset trades a bit of canvas height for a "full SaaS" chrome look.