/* style.css — cookbook/6-canvas-only — `.demo-mini-builder--canvas-only`.
*
* Self-contained chrome variant (W6.C.B.6). 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--canvas-only` rules below ARE
* the variant — copy them verbatim into your own stylesheet.
*
* ─── Buyer use case ─────────────────────────────────────────────────
* "No chrome at all." Canvas takes the entire wrapper area; the wrapper
* itself drops border + radius + background so the page reads as a
* bare-bones render — JUST the page, no surrounding editor frame.
* NO widgets palette, NO settings panel, NO header. Pick this when:
*
* - The host app drives ALL mutations programmatically (server-side
* AI generator, headless test rig, integration suite, ML pipeline).
* - The host app already has its OWN UI shell for actions and only
* needs the engine to render + accept programmatic edits.
* - You're embedding the builder inside another tool that paints its
* own chrome around the canvas (Storybook, internal admin tool, etc).
*
* The cookbook entry's host toolbar with "Select first" / "Toggle
* preview" / "Save" buttons demonstrates the ALL-API pattern: every
* action a chrome-rich variant offers via UI is reachable via a JS
* method on `builder`. The toolbar lives OUTSIDE the wrapper; the
* wrapper itself stays clean of UI.
*
* ─── Constructor (DOUBLE null arg) ─────────────────────────────────
* new Builder({
* mainContainer: '#YourCanvas',
* widgetsContainer: null, // no palette
* settingsContainer: null, // no panel
* });
*
* The W6.A.A.1 null-tolerance contract skips both sub-system mounts
* cleanly — no NPE, no console error, no empty-shell DOM.
*
* ─── Tokens consumed (already exposed by dist/builder.css) ──────────
* --bjs-bg, --bjs-bg-subtle, --bjs-border-light, --bjs-radius-md,
* --bjs-text, --bjs-text-secondary, --bjs-fs-sm, --bjs-fs-base,
* --bjs-space-2, --bjs-space-3, --bjs-primary.
*/
/* ─── Base wrapper (single-cell grid; canvas takes everything) ──────
* Same single-cell shape as the `--sidebar-only` base; the variant's
* own modifier strips the framing (border + radius + bg) so the
* wrapper visually dissolves and the page render reads as bare-bones.
* Height stays explicit because the engine's iframe needs a sized
* host — `100%` would collapse to 0 inside an auto-sized parent.
*/
.demo-mini-builder {
display: grid;
width: 100%;
height: 520px;
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 {
grid-area: canvas;
background: var(--bjs-bg);
overflow: auto;
min-height: 0;
min-width: 0;
}
/* ─── Variant: --canvas-only ── strip the frame ─────────────────────
* The variant's defining property: no border, no radius, no
* background. The wrapper visually dissolves; only the canvas
* remains. A buyer reading the page sees the engine's render, not
* a bordered editor card. This is THE rule that distinguishes
* `--canvas-only` from every other variant — locked by the
* cookbook spec's variant-defining-property gate.
*/
.demo-mini-builder--canvas-only {
border: none;
border-radius: 0;
background: transparent;
}
.demo-mini-builder--canvas-only .demo-mini-builder__canvas {
background: transparent;
}
/* ─── Host toolbar (lives OUTSIDE the wrapper) ──────────────────────
* The variant has zero in-wrapper chrome, so EVERY action button —
* Save, plus the API-demo buttons that prove the engine is fully
* API-drivable — lives in a host toolbar above the canvas. Same
* structural convention as `2-compact-2col`, `3-compact-3col`, and
* `5-sidebar-only` (variants without an `__header` slot).
*
* Three buttons by default:
* - "Select first element" — calls `builder.selectElement(...)`,
* proving selection drives canvas highlights even with no
* settings panel.
* - "Toggle preview" — flips `builder.setMode('design'|'preview')`,
* proving mode changes are API-driven.
* - "Save" — `getHtml()` + `getData()` round-trip,
* same Save pattern as every other cookbook entry.
*/
.demo-host-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
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;
}
/* Pressed state for the Toggle Preview button — `aria-pressed="true"`
* means we're currently in preview; tint the button so the buyer sees
* the engine's mode at a glance even with no header/status indicator.
*/
.demo-host-toolbar button[aria-pressed="true"] {
background: var(--bjs-bg-subtle);
border-color: var(--bjs-primary);
color: var(--bjs-primary);
}
@media (prefers-reduced-motion: reduce) {
.demo-mini-builder, .demo-mini-builder *,
.demo-host-toolbar button { transition: none; }
}
Canvas only — no chrome, programmatic-edit only
No widgets palette. No settings panel. No header. The wrapper has no border, no radius, no background — just the canvas. Every action — selection, mode toggle, save — comes from a method on <code>builder</code>. Pick this when your host app drives mutations programmatically (server-side AI generator, headless test rig, integration suite) or when the host already paints its own chrome around the canvas.
Walkthrough
-
Wrap one slot — canvas only, with a host toolbar above for API actions
The
--canvas-onlyvariant has ONE slot:__canvastakes the entire wrapper area. The variant CSS strips the wrapper's border + radius + background so the page reads as bare-bones — buyers SEE the engine rendering, no editor frame around it. Every button — including Save — lives OUTSIDE the wrapper in a.demo-host-toolbar(same convention as2-compact-2col/3-compact-3col/5-sidebar-only— variants without an in-wrapper header slot).HTML<div class="demo-host-toolbar"> <span class="demo-host-toolbar__title">Programmatic builder</span> <button type="button" id="CookbookCanvasOnlySelect">Select first element</button> <button type="button" id="CookbookCanvasOnlyTogglePreview" aria-pressed="false">Toggle preview</button> <button type="button" id="CookbookCanvasOnlySave">Save</button> </div> <div class="demo-mini-builder demo-mini-builder--canvas-only"> <div class="demo-mini-builder__canvas" id="CookbookCanvasOnlyCanvas"></div> </div> -
Mount canvas only — DOUBLE null arg
Both
widgetsContainerANDsettingsContainerarenull— the W6.A.A.1 null-tolerance contract skips both sub-system mounts cleanly. The engine boots in'preview'mode (clean WYSIWYG, no debug overlays); the API-demo buttons in the host toolbar work in either mode (programmatic selection + mode-toggle bypass the click flow). The "Toggle preview" button starts in preview state — clicking flips to design.JavaScriptvar builder = new Builder({ mainContainer: '#CookbookCanvasOnlyCanvas', widgetsContainer: null, // no palette settingsContainer: null, // no settings panel historyUI: false }); builder.load( window.THEME_JSON, window.THEME_TEMPLATES, window.THEME_CONFIG_DATA, window.MEDIA_URL, function () { // builder.setMode('design'); // opt-in: design-on-boot wireApiDemoButtons(builder); } ); -
Wire
Select first element— programmatic selection drives canvas highlights without a settings panelReach into
builder.pageElement.blocks[0]for the first block, then its.elements[0]for the first leaf element, and callbuilder.selectElement(target). Even though there is NO settings panel to render controls into, the iframe DOM still gets[data-bjs-selected="true"]and the engine's selection outline renders on the canvas. Proves the pitch: selection works independently of chrome.JavaScriptdocument.getElementById('CookbookCanvasOnlySelect').addEventListener('click', function () { var firstBlock = builder.pageElement.blocks[0]; var firstChild = firstBlock && firstBlock.elements && firstBlock.elements[0]; var target = firstChild || firstBlock; if (!target) { alert('Page has no elements to select.'); return; } builder.selectElement(target); }); -
Wire
Toggle preview— driven by themode:changedeventFlip between
'design'and'preview'viabuilder.setMode(). Subscribe to the W6.A.A.4mode:changedevent so the button label +aria-pressedreflect the engine state — single source of truth, not a click-handler-local toggle. Same reactive pattern as the4-rich-3col-headerPreview button.JavaScriptvar btn = document.getElementById('CookbookCanvasOnlyTogglePreview'); btn.addEventListener('click', function () { var next = builder.getMode() === 'preview' ? 'design' : 'preview'; builder.setMode(next); }); // Reactive label + aria-pressed — engine fires, button reflects. builder.events.on('mode:changed', function (e) { var previewing = e.to === 'preview'; btn.textContent = previewing ? 'Edit' : 'Toggle preview'; btn.setAttribute('aria-pressed', previewing ? 'true' : 'false'); }); -
Wire
Save—getHtml()+getData()round-tripSame Save pattern as every other cookbook entry — the alert pops with the round-trip output; production hosts swap the alert for a
fetch()POST to their backend.JavaScriptdocument.getElementById('CookbookCanvasOnlySave').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/6-canvas-only.
//
// Mounts a Builder instance into the `.demo-mini-builder--canvas-only`
// 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
//
// DOUBLE null arg — `widgetsContainer: null` AND `settingsContainer: null`
// (no palette, no settings panel). The W6.A.A.1 null-tolerance contract
// skips both sub-system mounts cleanly. The wrapper holds ONLY the
// canvas — no in-wrapper chrome at all.
//
// Save + API-demo buttons live OUTSIDE the wrapper in a host toolbar
// (same convention as `2-compact-2col` / `3-compact-3col` / `5-sidebar-only`):
//
// - Select first element → builder.selectElement(...)
// - Toggle preview → builder.setMode('design'|'preview')
// - Save → getHtml() + getData() round-trip
//
// Engine boots in 'preview' mode → call `setMode('design')` after `load()`
// so the canvas accepts edits + selections from the first click.
// (See `cookbook/4-rich-3col-header/` for the discovery context.)
(function () {
'use strict';
var canvas = document.getElementById('CookbookCanvasOnlyCanvas');
if (!canvas || typeof window.Builder !== 'function') return;
var builder = new window.Builder({
mainContainer: '#CookbookCanvasOnlyCanvas',
widgetsContainer: null, // no palette
settingsContainer: null, // no settings panel
historyUI: false
});
builder.load(
window.THEME_JSON,
window.THEME_TEMPLATES,
window.THEME_CONFIG_DATA,
window.MEDIA_URL,
function () {
// 2026-05-09 — demo defaults to 'preview' (engine default)
// per the "no debug overlays unless showcase" rule. The host
// toolbar's "Toggle preview" button + the "Select first" API
// demo button still drive the engine; this variant shows the
// canvas-only chrome, not mode-toggle as a feature.
wireApiDemoButtons(builder);
// Reactively reflect engine mode on the Toggle Preview button —
// single source of truth (engine fires `mode:changed`, button
// reflects). Same pattern as `4-rich-3col-header` Preview.
builder.events.on('mode:changed', function (e) {
applyPreviewState(e.to);
});
// Expose for buyer-side debugging + spec assertions.
window.cookbookCanvasOnlyBuilder = builder;
}
);
/**
* Wire the host toolbar's three API-demo buttons. Each handler is
* independent — copy/drop into your own host shell freely.
*
* @param {Builder} b
*/
function wireApiDemoButtons(b) {
var selectBtn = document.getElementById('CookbookCanvasOnlySelect');
var previewBtn = document.getElementById('CookbookCanvasOnlyTogglePreview');
var saveBtn = document.getElementById('CookbookCanvasOnlySave');
// (1) Select first element — drives the canvas selection
// highlight programmatically. Even though there is NO settings
// panel to render controls into, the iframe DOM still gets
// `[data-bjs-selected="true"]` and the engine's selection
// outline renders on the canvas. Proves: selection works
// independently of chrome.
if (selectBtn) {
selectBtn.addEventListener('click', function () {
var firstBlock = b.pageElement && b.pageElement.blocks && b.pageElement.blocks[0];
var firstChild = firstBlock && firstBlock.elements && firstBlock.elements[0];
var target = firstChild || firstBlock;
if (!target) {
alert('Page has no elements to select.');
return;
}
b.selectElement(target);
alert(
'Selected · ' + (target.getName ? target.getName() : 'element') +
' · highlight rendered on canvas (no settings panel needed).'
);
});
}
// (2) Toggle preview — flips engine mode. The button label +
// aria-pressed reflect the engine state via the `mode:changed`
// listener above; here we just call setMode().
if (previewBtn) {
previewBtn.addEventListener('click', function () {
var next = b.getMode() === 'preview' ? 'design' : 'preview';
b.setMode(next);
});
// Initialise label + aria-pressed for the boot mode.
applyPreviewState(b.getMode());
}
// (3) Save — same round-trip pattern as every other cookbook
// entry. Production hosts swap the alert for a fetch() POST.
if (saveBtn) {
saveBtn.addEventListener('click', function () {
var html = b.getHtml();
var data = b.getData();
alert(
'Saved · ' + html.length + ' chars HTML, ' +
Object.keys(data).length + ' top-level page-tree keys.'
);
});
}
}
/**
* Reflect the engine's mode on the Toggle Preview button.
* `aria-pressed="true"` when previewing → CSS in style.css tints the
* button so the buyer sees the active state even without a header
* status indicator.
*
* @param {string} mode 'design' | 'preview'
*/
function applyPreviewState(mode) {
var btn = document.getElementById('CookbookCanvasOnlyTogglePreview');
if (!btn) return;
var previewing = mode === 'preview';
btn.textContent = previewing ? 'Edit' : 'Toggle preview';
btn.setAttribute('aria-pressed', previewing ? 'true' : 'false');
}
})();
Notes
Why no chrome at all? Three real-world fits. (1) Headless render hosts — server-side AI generators or template seeders that emit page JSON, then mount the engine purely to call getHtml() for export; no human ever sees the canvas. (2) Embedded inside another tool — Storybook stories, internal admin pages, integration tests where the surrounding tool already paints chrome and just needs the engine inline. (3) Custom UX — your host app has its own design system and wants to wire its own buttons to builder.* methods rather than adopt the cookbook's chrome variants. The canvas-only variant is the maximum-flexibility option: zero opinions about chrome, full access to the engine's API surface.
Why does the wrapper drop border + radius + background? The point of "no chrome" is the page looks like a real page render, not a bordered editor card. With the framing dropped, the canvas blends into the host page; visitors don\'t parse the boundary "this is the editor area" because there isn\'t one. If your host app DOES want a subtle frame (e.g. a dashboard tile feel), pick 8-card instead — that variant ships a card-shaped wrapper purpose-built for tile layouts.
Why does the host toolbar exist if the variant pitch is "no chrome"? The toolbar is OUTSIDE the wrapper — it\'s host chrome, not engine chrome. The wrapper itself stays clean. The toolbar exists in this cookbook entry purely as a teaching aid: it demonstrates that every action a chrome-rich variant offers via UI (Save, Preview, Selection) is reachable via a method on builder. In your real host app, drop the toolbar entirely if your host doesn\'t need a UI affordance — just call builder.selectElement(...) / builder.setMode(...) / builder.getHtml() from your own code paths (route handlers, button listeners elsewhere on the page, AJAX callbacks, etc.).
Why builder.pageElement.blocks[0] for "Select first"? The page tree shape is PageElement → BlockElement[] → (BlockElement.elements OR GridElement.cells → CellElement.elements) → leaf elements. The first block\'s first child element is the natural "first thing on the page". For more sophisticated walks, see the engine\'s tree-traversal helpers (pageElement.getChildren(), BlockElement.getElements()) — but plain array indexing is enough for "select something to prove selection works".
Why subscribe to mode:changed instead of just toggling the button label inside the click handler? Single source of truth. If anything else flips the mode (a programmatic call elsewhere, a future keyboard shortcut, an integration test), the button still reflects the truth. Click-handler-local toggles silently lie when the engine\'s mode flips through a different path. This is the same pattern the 4-rich-3col-header Preview button uses — see DISCOVERIES D13.C.B.4 for the full rationale.
Why setMode(\'design\') after load()? The engine boots in \'preview\' mode (read-only WYSIWYG). Without the flip, "Select first" still works (selection is a separate API path), but clicks on the canvas itself don\'t open settings — and in this variant there\'s no settings panel anyway, so the buyer might not notice. Setting design mode keeps the canvas behaviour consistent with sibling cookbook entries: clicks select, drags reorder, etc. (See 4-rich-3col-header notes for the full discovery.)
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/SitewideSale\') and emits the four named globals before init.js loads. In your own project, render them from any backend — only the four names matter to the engine. See Quickstart for the canonical pattern.
Want to drive mutations programmatically too? The engine exposes builder.insertElements(elements) for inserts, builder.removeElement(element) for removes, builder.history.undo() / builder.history.redo() for history nav, plus the full element tree at builder.pageElement. Wire your own host UI to those — same as this entry wires Select first / Toggle preview / Save. The pattern scales as far as your host app needs.