Pagecraft Changelog
PageCraft Changelog
All notable changes to this project are documented in this file.
The format follows Keep a Changelog. Versioning follows Semantic Versioning.
[1.1.25] — 2026-08-21
Fixed
- Three block images that were referenced but never shipped.
office2.png,person2.pngandai-sFtJP2.jpgare referenced by blocks inminimalist-blocks(and, for the last one, by a Simple template) but were absent from the theme, so those blocks rendered broken images. Recovered from the ContentBox vendor distribution and added toassets/minimalist-blocks/images/.
Notes
- A sweep of every asset reference in the block library and the three template packs found 110 references with no corresponding file, of which only these 3 exist anywhere in the vendor distribution. The remaining 107 — 70
-edit*variants inminimalist-blocks, 29img-0NN.jpgintemplates-animated, and a handful elsewhere — were never shipped by InnovaStudio and cannot be recovered. Blocks and templates depending on them will show a missing image regardless of PageCraft version.
[1.1.24] — 2026-08-20
Added
{exp:page_craft:playground}— a private scratchpad: the full PageCraft editor on a front-end template, detached from any entry, for building and trying out layouts without creating a throwaway entry to hold them.- Super admin only. Any other member, and every logged-out visitor, gets an empty string — so dropping the tag on a public template exposes nothing. The check lives in the tag rather than being left to template logic, because a tag that renders an editor to the public when someone forgets an
{if}is the wrong default.editor_frameperforms its own auth check as well, so this is the second lock rather than the only one. - It embeds the real
editor_frameendpoint, not a stripped copy, so uploads go to the EE file manager, the file picker works, and AI is available if enabled. - Content is not saved to an entry. It round-trips through
localStoragevia the samepostMessageprotocol the publish form uses (pagecraft-ready/pagecraft-init/pagecraft-save), so work survives a reload. Copy HTML lifts the markup and CSS out when a layout is worth keeping; Reset clears it. - Optional
heightparameter, in px, clamped 400–2000 (default 800).
- Super admin only. Any other member, and every logged-out visitor, gets an empty string — so dropping the tag on a public template exposes nothing. The check lives in the tag rather than being left to template logic, because a tag that renders an editor to the public when someone forgets an
[1.1.23] — 2026-08-18
Fixed
- Nav item hover colour and dropdown hover background work again. Both settings were being written correctly; the dark-theme block in
nav-menu/style.csswas overriding them with hardcoded values at far higher specificity. Its selectors are(0,6,1)—div.is-section.nav-menu.dark [data-cb-type="nav-menu"] .nav-link:hover— while the variable-driven rules that read your settings are(0,3,0), so a literal there silently beat anything set in the Nav Items modal. Worse, dark's non-hover.nav-link { color: #f5f5f5 }outranked the hover rule entirely, which is why hover colour did nothing at all. Every colour in the dark block is now expressed asvar(--nav-*, <dark default>), so a set value wins and the dark default applies otherwise. Only ever affected navs using the dark theme. - Hover background is no longer discarded by the active style. The Boxed, Line and Caret styles hardcoded their hover background (
background: transparent, or a separate--active-bg-color), throwing away--nav-link-hover-bg. They now read the user's value with the stock colour as fallback. This one affected light navs too.
Added
- Highlight colour. A single control driving the Line style's underline bar, the Boxed style's bottom border, and the dropdown item's hover indicator — one value, three consumers, so they cannot drift apart. These were previously fixed defaults in the stylesheet with no control at all.
- Logo size, 40–100 px.
.logo-imgand its rounded variant both track--nav-logo-heightrather than the hardcoded40px/49×49. The bar auto-grows to fit: its height ismax(nav height, logo + 20px), computed in CSS so the slideout's Nav height slider and this control stay independent. - Bar margin and corner radius, for a floating nav bar. The bar insets from the section edges and its width shrinks by the horizontal margin so it can't cause a horizontal scrollbar.
- Bar background, used when the bar floats. A floating bar can't inherit the section background — it would be an invisible pill in the colour behind it — so a margin plus this colour gives the floating look. With no margin the nav still inherits the section background exactly as before, preserving the behaviour locked in v0.9.40/v0.9.45.
- All five controls live in a new BAR tab in the Nav Items modal, and round-trip through
data-nav-*attributes like the rest of the nav settings.
Notes
- This release changes theme assets. Unlike 1.1.14–1.1.22, installs must copy
themes/user/pagecraft/again (and CDN-fronted sites should purge the pull-zone) or the nav changes won't appear.
[1.1.22] — 2026-08-18
Changed
- Demo photos and videos ship as local placeholders. ContentBox's stock demo media — ~48 MB of photos and video whose only job is to make a freshly dragged-in block look finished — is replaced at build time with flat grey placeholders of the same aspect ratio, and a blank clip for video. 534 images and 8 videos, 48.4 MB down to 3.0 MB.
- Filenames and formats are unchanged. Nothing is renamed or deleted, so no reference needs rewriting anywhere — not in PageCraft's code, not in ContentBox's — and pages already built on an existing install keep resolving their images, rendering a grey box rather than a broken one.
- Aspect ratio is preserved per image (read with
sips, 45 distinct ratios in this build), so a portrait photo is replaced by a portrait placeholder and block layouts don't shift. Placeholders are generated at 640px on the long edge; being a flat fill, they are visually identical to full-size ones once scaled by the block's CSS. - The editor's existing
pc-imgtreatment still applies, so an unset image reads as a placeholder zone rather than a missing asset. - Excluded: the top-level
assets/images/, which holds the animation preview GIFs used by the editor UI, not demo content. - The build degrades gracefully — without
sips(non-macOS) it warns and ships the full-size media.
Notes
- Package is now 20 MB zipped / 42 MB unpacked, down from 90 MB / 112 MB at 1.1.19 — a 78% reduction across 1.1.20–1.1.22.
- This removes the need for the video re-encoding step considered in 1.1.21: the demo videos are replaced outright rather than compressed.
[1.1.21] — 2026-08-18
Changed
- Picker thumbnails ship as WebP.
build-release.shconverts every*/preview/image to WebP at quality 82 and rewrites the references — 412 thumbnails, 10 MB down to 5 MB. Verified to introduce no broken references: the count of unresolved image references in each template pack is identical before and after conversion.- Only
*/preview/is converted.*/images/is deliberately left alone — those photos are inserted into user page content, so a customer's saved HTML points at.../images/foo.jpgand changing the extension would 404 every page already built on an existing install.styles/andfonts/are also skipped; their paths are constructed inside ContentBox's own minified JS. - A thumbnail is only converted when its basename is unique across the package, so the reference rewrite cannot touch an unrelated file of the same name. 151 were skipped on that basis.
- The build degrades gracefully: without
cwebpon PATH it warns and ships the original PNG/JPG.
- Only
Removed
minimalist-blocks/example.mp4(240 KB), referenced by nothing in the package.
Notes
- Package is now 67 MB zipped / 87 MB unpacked, down from 90 MB / 112 MB at 1.1.19.
- Video re-encoding was attempted and abandoned for now: macOS's
avconvertuses fixed-bitrate presets, so re-encoding these already-compressed sources made them larger at 540p and 720p, and only 480p saved space — unusable for full-width background video. Doing this properly needsffmpegwith CRF-based encoding.
[1.1.20] — 2026-08-18
Removed
- Monochrome Blocks is no longer bundled or shipped. It is a separately licensed ContentBox.js add-on and is not covered by the redistribution rights PageCraft ships under, so it must not travel in the package. The pack is now gitignored, untracked, and excluded by
build-release.sh; the files stay on disk for local development. No code change was needed — packs are auto-discovered frompack.json, so nothing referenced it by name. - Package size drops from 90 MB to 75 MB zipped (112 MB to 95 MB unpacked).
[1.1.19] — 2026-08-16
Changed
-
The release package now mirrors ExpressionEngine's own folder structure.
build-release.shused to emit a singlepage_craft/folder with the theme tree buried inside it, leaving the customer to find it and copy it out by hand. The package is now:PageCraft-<version>/ INSTALL.md system/user/addons/page_craft/ themes/user/pagecraft/Copy both folders into the site root and install from the control panel — the manual
cp -Rstep is gone. Hoisting is safe because nothing reads the add-on's bundled copy at runtime:ThemeAssets::getThemePath()resolves the site's owntheme_folder_path, and the install-time auto-sync that once read the bundled tree was removed in 1.1.11.
Added
INSTALL.md, shipped at the package root: requirements, install and upgrade steps, verification, and troubleshooting for the two failure modes that actually happen — a missing theme tree, and an upgrade that didn't replace it (including the CDN pull-zone note for Cloudways-hosted sites).
Notes
README.mdnow documents both paths: installing from a release package, and working from the repo (where the theme tree still lives inside the add-on and still needs copying to the site's theme folder after any editor-asset change).
[1.1.18] — 2026-08-13
Fixed
- ContentBox's tool-rail icons are legible in dark mode. 1.1.17 turned the rail dark but left the icons near-black on it. ContentBox's UI icons are sprite symbols (
<svg class="svg-icon"><use href="#icon-…">) and every symbol is definedstroke="currentColor" fill="none"— so they are painted by the CSScolorproperty. ContentBox's own dark rule setsfill(.dark #_cbhtml .is-sidebar.is-area button svg { fill: #d3d3d3 !important }), which those paths ignore, and it ships no dark override forcolor. PageCraft now drivescolorinstead, which fixes every sprite icon in the chrome at once. Scoped to:root.dark, so light mode is untouched.
[1.1.17] — 2026-08-12
Added
- The editor and global-component frames follow the CP theme. Both are standalone documents that can't inherit the control panel's
body[data-theme], but they are same-origin with the CP, so they now read the samelocalStorage.themekey EE's account-menu switcher writes. A short<head>script applies it todocumentElementbefore first paint — the same anti-flash approach EE uses inheader.php.- Setting
darkon<html>also switches on ContentBox's own dark chrome: 96 rules incontentbox.cssand 17 incontentbuilder.cssare scoped to a.darkancestor, covering the left tool rail and the section/box settings panels. PageCraft only themes the UI it injects itself. - Toggling the CP theme updates an already-open editor, no reload — the
storageevent fires in every same-origin document except the one that changed the value. - The Save as Section modal is themed (converted from inline styles to classes).
- The canvas document is deliberately excluded: it renders real page content and must keep showing the design as visitors will see it.
- Setting
Changed
- The theme bootstrap and PageCraft's mirror of EE's design tokens live in
Traits/ThemeAssets(cpThemeBootstrap(),cpThemeTokensCss()) so the two frames can't drift apart. The mirror exists only because a standalone frame doesn't load EE's control-panel stylesheet; anything rendering inside the CP body uses EE's tokens directly. - The publish-form toolbar now uses
--ee-gray-5rather than--ee-gray-blank. In dark mode pure black made the toolbar the darkest element on screen, below even the CP page behind it.
Known gaps
- The MCP control-panel pages (Settings, Sections, Library, Docs) still carry hardcoded colours and stay light under a dark CP theme.
- Within the editor frame, the right-click context menu, the lock/unlock buttons and the toast are not themed yet.
[1.1.16] — 2026-08-12
Added
- The publish-form chrome follows the CP theme. The PageCraft field's wrapper, toolbar and canvas surround now take their colours from ExpressionEngine's own control-panel design tokens (
--ee-gray-*,--ee-text-*) instead of hardcoded hex. Switch the CP theme from the account menu — Dark, Snow, Slate, Pink — and the field follows, with no PageCraft setting to configure and no JavaScript involved: EE defines the tokens on:rootand re-defines them per theme onbody[data-theme=*], and the field renders inside that body, sovar()resolves on its own.- The editor canvas is deliberately untouched. It renders real page content in its own iframe document and must keep showing the design as visitors will see it. Only the chrome around it is themed.
- Each token carries the previous hardcoded value as its
var()fallback, so the chrome still renders sanely if EE renames a token.
Changed
- The field's toolbar, buttons and canvas surround are styled from a stylesheet (emitted once per request, however many PageCraft fields are on the page) rather than inline
styleattributes, and the active-device button is tracked with anis-activeclass rather than JavaScript writingstyle.background/style.color. Inline styles can't be overridden by a themed rule, so this was a prerequisite for theming. Light-mode appearance is unchanged apart from the toolbar icons, which now use EE's--ee-text-tertiary(slightly darker and cooler than the previous#999, and better contrast in both themes).
[1.1.15] — 2026-08-05
Fixed
- Buttons in ingested templates are editable again. ContentBox only edits an
<a>as a button when the anchor sits inside a cell carryingis-subblock editwhose computed display is block, inline-block or flex. The ingest pass setrole="button"on button-like elements — which ContentBox ignores — and then excluded those elements from the loose-text pass that hands out editable markers. The combination froze every CTA in an imported template: label uneditable, URL unchangeable.- Ingest now marks the button's parent as the editable cell (
prepareSectionDom()step 4b), which is the shape ContentBox actually looks for. The anchor itself is never marked — doing so makes ContentBox treat it as a text block and drops the button settings panel. - The cell is only claimed when it holds nothing but button-like children (whitespace, comments and
<br>ignored), so a mixed block — heading + copy + button — is left alone rather than handed to ContentBox whole. Inline parents (span,label) are skipped because they can't satisfy the display requirement, as are structural nodes (section / container / column), which would need a real wrapper element that this pass deliberately does not create. <button>elements are still left alone. ContentBox will only ever edit an<a>, so the only fix would be converting<button>→<a>, which breaks form submits; FormBuilder is the supported path for forms.- Note: this changes ingest-time output. Existing sections must be re-ingested to pick it up; a code deploy alone won't alter sections already in the library.
- Ingest now marks the button's parent as the editable cell (
[1.1.14] — 2026-08-05
Fixed
- Imported sections no longer lose their thumbnail on save. Sections created by Pack Import or Template Ingest store their thumbnail as a plain URL, not an EE file reference. EE's drag-and-drop file field can't render a bare URL — it showed a broken red X — and it posts back empty, so simply opening a section and clicking Save silently wiped the thumbnail. The section-edit screen now detects a URL-shaped thumbnail, renders it as a direct
<img>preview ("Imported preview. Upload below to replace."), and carries the value through a hidden field thatsave_section()falls back to when the picker is empty. The picker is still offered for replacement. The hidden field is client-supplied, so its URL shape is re-validated server-side before it's accepted. - The section picker shows section names again. The generated sections file emitted
thumbnail,categoryandhtmlfor each design but noname, so ContentBox's picker rendered unlabeled thumbnails. The name is now emitted, encoded withjson_encode()so names containing quotes, backslashes or line breaks still produce a valid JS string literal.- Note: this changes generated output. Existing installs must regenerate the sections file (PageCraft → Sections → Regenerate sections file) before titles appear; a code deploy alone won't do it.
[1.1.13] — 2026-07-14
Fixed
- Editor module fetches are now cache-busted by add-on version. ContentBox loads the FormBuilder settings panel (and other modules) by fetching
assets/modules/<name>.htmlfrom a bare, cacheable URL. On hosts with a CDN/Varnish in front of the theme (e.g. Cloudways with CloudwaysCDN), that file could be served stale to the editor even when the origin was updated — so new editor fields (like 1.1.12's Redirect URL) didn't appear until the CDN pull-zone was purged, which is easy to miss behind the usual Varnish/browser purges. The three ContentBox module fetches now append?v=<add-on version>(window.PC_ASSET_V, injected by the editor and global-component frames). A new query string is a new URL to browser, Varnish, and CDN, so every version bump forces a fresh pull automatically.- One-time note: the browser/CDN must load this patched
contentbox.min.jsonce for the buster to take effect. On a CDN-fronted site, purge the CDN pull-zone after deploying 1.1.13; from then on, module updates propagate without manual purges.
- One-time note: the browser/CDN must load this patched
Changed
- Editor and global-component frames now expose
window.PC_ASSET_V(the add-on code version) for the module cache-buster.
[1.1.12] — 2026-07-14
Added
- FormBuilder: optional "Redirect URL (on success)". A new field in the FormBuilder Settings tab. When set, a successful submission sends the visitor to that URL (
/thank-you, a full URL, etc.) instead of showing the inline Thank You Message. Leave it blank to keep the current behavior (the message is shown). The value is stored per form in the block'sdata-settings(redirectUrl) and escaped for the generated form script. Applies to the default JSON submission type.
Notes
- The generated form still passes a block of unused InnovaStudio FormViewer options (AI media generation:
model, Fal/OpenAI endpoints, and the disabled Workflow tab). These render as empty strings and are harmless; they were left in place to avoid altering vendor FormViewer expectations. No functional config beyond the fields exposed in the Settings tab is active.
[1.1.11] — 2026-07-13
Changed
- Reverted the automatic theme-asset sync (1.1.9–1.1.10). It proved unreliable across hosting setups. PageCraft's theme assets are again installed/updated by manually copying the bundled theme into the active theme, on install and after any upgrade that changes editor/runtime assets:
See the README for the full step.cp -R system/user/addons/page_craft/themes/user/pagecraft/* themes/user/pagecraft/
[1.1.10] — 2026-07-13
Fixed
- Editor assets self-heal — the theme sync no longer depends on running the CP "update". 1.1.9 copied the bundled theme into the active theme only during
update(), which was still fragile (the update had to actually run, with the bundled theme deployed). PageCraft now checks and repairs the active theme (themes/user/pagecraft/) wherever the editor is about to load — the fieldtype's publish-form render, the settings page, and the updater — viaThemeAssets::ensureThemeAssets(). It's a cheap version-stamp comparison (.pc-assets-version) on the happy path and copies only when the stamp is missing/old or the ContentBox core asset is absent; the per-installpagecraft-sections/andimported/trees are skipped and nothing is deleted. The stamp uses the add-on's code version (fromaddon.setup.php), so simply dropping a newer build on disk triggers the re-sync on the next editor load — no CP update required. If the copy can't run, the exact reason (e.g. "themes/user/pagecraft/ is not writable") is now shown in the CP instead of failing silently.
[1.1.9] — 2026-07-13
Fixed
- Theme assets now sync automatically on install/update — no more manual
cp. The ContentBox editor loads its module/runtime files from the site's active theme (themes/user/pagecraft/), a separate copy from the add-on's bundled theme that agit pull/ CP update never touched. So an upgrade that changed an editor asset (e.g. the 1.1.8 FormBuilder settings panel) silently didn't take effect until someone re-ran the README'scp -R. The updater now copies the bundled theme into the active theme on install and update. Add-on-owned files are overwritten (they must match the installed version); nothing is deleted and the per-installpagecraft-sections/(generated) andimported/(user packs) trees are skipped. Best-effort — a copy failure is logged, never fatal.
[1.1.8] — 2026-07-13
Added
- Per-form notification recipient. Each FormBuilder form can now send its submissions to its own email address instead of one site-wide recipient. Set it in Form Builder → Settings tab → "Notification Email". The global setting (Add-Ons → PageCraft → Global Settings) is retained as the default/fallback — resolution order is per-form → global default → the site's
webmaster_email.- The recipient is encrypted server-side at render (
ee('Encrypt'), injected as a hidden_pc_to_encfield byThemeAssets::rewriteFormActions()), so it never appears in page source and cannot be forged — a crafted direct POST can't redirect a site's form mail to an arbitrary address. A plaintext value is never trusted. No database migration is required; the recipient travels with the form markup.
- The recipient is encrypted server-side at render (
[1.1.7] — 2026-07-05
Fixed
- Full pages no longer fail to save with "Data too long for column". PageCraft's field stored into a
TEXTcolumn (64KB max) — itsinstall()declaredmediumtext, but that key only sets a fieldtype default and is never applied to the per-field data column, so EE kept building/reverting it toTEXT. A page with many (or large) sections overflowed it on save. The fieldtype now implementssettings_modify_column()to pin the storage column toMEDIUMTEXT(16MB), and the updater widens existing PageCraft field columns on upgrade. - Import Pack (.zip) no longer fails with "Invalid server response" (403). The Import-Pack modal read its CSRF token from a global (
EE_CSRF_TOKEN) that isn't defined on the Sections page, so it POSTed an empty token and EE'sStrict_XIDrejected the request with a 403 (HTML, not JSON). The token is now embedded server-side viaCSRF_TOKEN, matching the Ingest Template modal.
Added
- Placed sections are identifiable in the markup. Each section's HTML now begins with an
<!-- PageCraft Section: {name} -->comment (injected as the root's first child), so a section can be recognized in the page/canvas source.
[1.1.6] — 2026-06-30
Fixed
- Imported packs' lazy-loaded images now render in the editor canvas. Packs that lazy-load images (vanilla-LazyLoad, Arts/Kinsey, etc.) keep the real URL in a
data-*attribute and rely on a front-end script to swap it intosrc/backgroundon scroll. That script never runs inside the ContentBox iframe, so images stayed on their#/empty placeholder and rendered blank while editing. The canvas now force-loads them on load (and via aMutationObserver, so sections inserted while editing also resolve), coveringdata-src/data-srcset/data-original/data-lazy(imgs) anddata-bg/data-background[-image](CSS backgrounds). Canvas-only — the front end is untouched. - JS-driven masonry grids no longer collapse to a stacked column in the canvas. Grids laid out by a front-end library (Isotope/Masonry, the Arts framework's
.grid.js-grid) absolutely-position their items via JS that doesn't run in the canvas, so the items — which carry only flex-basis/max-width in a non-flex container — stacked. The canvas now applies a flex-wrap fallback to.grid.js-grid/.isotope/.masonry/[data-masonry]so items flow by their column widths into a faithful, editable grid (rows align to the tallest item; not true masonry). Canvas-only; the front end's real layout JS is unaffected.
Both fixes live in Actions/EditorFrame.php (the canvas=1 branch), alongside the existing anime.js and Bootstrap grid-compat canvas shims.
[1.1.5] — 2026-06-22
Fixed
- No more full-height empty band before the footer on pages with imported sections. ContentBox gives every
.is-sectionmin-height:100vhand only resets it for auto-height sections via a DIV-scoped rule (.is-wrapper div.is-section-auto{min-height:unset}). Sections authored or imported as the semantic<section>tag (ContentBox marks themdata-new="true") never matched that reset, so a short<section class="is-section is-section-auto">was stretched to a full viewport height — producing a tall blank gap before the next block or the footer (seen across the location pages). PageCraft now emits a front-end compatibility rule (ThemeAssets::sectionAutoHeightCss()) that broadens the reset to any element carryingis-section-auto, regardless of tag. It only generalises ContentBox's own intent ("auto" = auto height), so<div>sections andis-section-100(which should stay 100vh) are unaffected. Emitted on the published render paths; the editor canvas keeps ContentBox's native behaviour.
[1.1.4] — 2026-06-20
Fixed
- FormBuilder forms now submit to PageCraft automatically — no endpoint to paste. A saved form stores a literal
action="…"from its "Form Action" setting, whose InnovaStudio default is the placeholder/formsubmission— not a real URL. Left on the default (or pointed at anACT=id from another environment), the form POSTed to a 404 whose HTML body maderesponse.json()throwUnexpected token '<': no storage, no email. This was the real cause behind the 1.1.1–1.1.3 saga; those releases fixed adjacent issues but the form was never reaching theform_submitendpoint. PageCraft now rewrites a FormBuilder form'sactionto this install's ownform_submitACT URL at render time (all front-end paths: field tag,{exp:page_craft:render}, and global header/footer). Editors can leave "Form Action" on the default and it Just Works on every environment.- Scope is tight: only
<form id="form_…" method="POST">tags on a page that contains a FormViewer form are touched; search/comment forms are left alone. A form whose action points at an absolute off-site URL is also preserved, so posting to a third-party service on purpose still works.
- Scope is tight: only
form_submitno longer 500s on its own logging.ee()->loggeris auto-loaded in the Control Panel but not in a bare public front-end ACT request, whereee()->logger->developer()throws "No such property: 'logger'" on the Legacy Facade. Every diagnostic log call in the endpoint (missing table, no recipient, send failed, and the top-levelcatchitself) therefore turned into an HTML 500 — which made the form'sresponse.json()choke onUnexpected token '<'. Logging now goes through a best-effortlogDev()helper that loads theloggerlibrary first and swallows any failure, so a log write can never error the visitor's form. (This is what surfaced once 1.1.4's rewrite finally routed submissions to the endpoint.)
[1.1.3] — 2026-06-18
Fixed
- Form submissions no longer 500 when the submissions table is missing. If the
pagecraft_form_submissionstable doesn't exist on an environment (e.g. the add-on update registered theform_submitaction but hasn't created the table yet), EE's database layer callsshow_error()and returns an HTML 500 before the endpoint's try/catch can produce JSON — which makes the form'sresponse.json()throwUnexpected token '<'. The endpoint now checkstable_exists()first: when the table is missing it still emails the submission and returns a clean JSON success, and logs that the update needs to be re-run, instead of crashing the visitor's form.
[1.1.2] — 2026-06-18
Fixed
- FormBuilder forms now actually render — load the viewer build, not the builder. 1.1.1 loaded
formbuilderai.min.json the front end, but that bundle definesFormBuilderAI(the editor) and contains noFormViewer— so the published form'snew FormViewer(...)still threwFormViewer is not defined. The globalFormVieweris defined by the separate viewer build (assets/scripts/formviewer/formviewer.min.js). The front-end loader now emitsformviewer.min.js+formviewer.cssinstead. (Both builds ship in the theme; only the viewer is needed on published pages.)
[1.1.1] — 2026-06-18
Fixed
- FormBuilder forms now render on the front end. A saved form's inline script calls
new FormViewer(...)to build its fields, butFormViewerlives informbuilderai.min.js— an editor asset that was never loaded on the published page, so forms threwFormViewer is not definedand showed nothing. The front-end runtime now loadsformbuilderai.min.js+.csswhenever the rendered content contains a FormBuilder module (both the field-tag and module render paths, via a sharedThemeAssets::formBuilderAssetTags(), loaded once per request). - The endpoint URL in Forms settings no longer shows
ACT=0before the action is registered. Whenform_submitisn't registered yet (e.g., the add-on update hasn't run on a given environment), the settings page now shows a "re-run the update" notice instead of a broken…ACT=0URL that would silently fail when pasted into a form.
[1.1.0] — 2026-06-18
Added
- Native form submissions for the FormBuilder. PageCraft's bundled FormBuilder (InnovaStudio FormBuilder AI) could already build forms in the editor, but had nowhere to submit to on a live site. A new public endpoint (
form_submitACT, registered csrf-exempt) now receives submissions, honoring FormBuilder's JSON contract (POSTFormData→ JSON, success = noerrorkey). Each submission is:- Stored in a new
pagecraft_form_submissionstable (fields as JSON + source URL, IP, user-agent, timestamp). - Emailed to a configurable recipient via ExpressionEngine's native Email library — so it uses the site's own Outgoing Email settings (PHP mail / Sendmail / SMTP). From is the site address, Reply-To is the submitter's email when present.
- Spam-guarded with a same-origin check, per-IP rate limit (10/min), optional reCAPTCHA v2 server-side verification, and a honeypot hook.
- Stored in a new
- Forms settings (Add-Ons → PageCraft → Global Settings): notification recipient email, reCAPTCHA v2 secret key (stored encrypted), and a copy-ready display of the endpoint URL to paste into each form's Form Action (Form Builder → Settings tab).
Notes
- v1 covers text fields; file-upload fields and an in-CP submissions viewer (the table already stores everything) are planned follow-ups. FormBuilder AI is a commercial InnovaStudio library — a developer license is required.
[1.0.0] — 2026-06-12
First stable release. Consolidates a round of security hardening, correctness fixes, and editor-UX work (versions 0.9.61–0.9.66 plus the additions below). Known limitation: the element-delete fix (0.9.66) is a runtime guard around a ContentBox behavior, not a change to how editable text is marked up.
Added
- Grid Editor (row & column editor) is now exposed in the canvas. ContentBox ships ContentBuilder's row-level Grid Editor — Insert/Delete Column, Columns per Line, Wrap/Reverse Columns, Row/Column IDs, Hide on Mobile — but hides its row-toolbar button by default (
.is-tool.is-row-tool .row-grideditor { display:none }; only itsminimal/cleanmodes reveal it). PageCraft now un-hides just that one button (in both the editor chrome and the canvas iframe), so selecting a row surfaces a grid icon that opens the full Grid Editor panel — without switching the whole editor into minimal mode. Cleanest on native.row > .columnblocks; works on template-ingested Bootstrap rows but interacts with the grid-framework conversion. - Toolbar Style setting (Colored / Monochrome). A new global setting (Add-Ons → PageCraft → Global Settings) renders the in-editor section/column/element toolbars in neutral gray instead of ContentBox's multi-color set, for a calmer canvas. Rather than ContentBox's native gray mode (which banded the tools, left icons white, and never reached the section-level tool), PageCraft drives it from a
pc-mono-toolsbody class + its own CSS across the canvas and editor chrome: neutral pills, dark icons, no background band, with the "Save as Section" button kept a distinct mid-gray. Default is Colored, so existing installs are unchanged.
[0.9.66] — 2026-06-12
Fixed
- Deleting an element no longer wipes text across the canvas. ContentBox's element-delete runs a sweep that walks every
.is-subblockand, for any whoseelementChildren()is empty (it counts element children only — text nodes don't qualify), replaces itsinnerHTMLwith aspacer height-40placeholder. PageCraft marks leaf text elements (h2/span/p) asis-subblock edit, so they hold a text node but no element child — ContentBox judged them "empty" and erased their text on any delete, canvas-wide (onlyis-subblockelements were affected; nav/hero/plain sections were untouched). A new subblock-content guard in the editor canvas (Actions/EditorFrame.php, behindwindow.pcSubblockGuardActive) remembers each text subblock's last non-empty content and restores it the instant ContentBox wipes it — before the debounced save fires, so real content is what persists. No vendor-code changes. Note: this prevents future wipes; content already saved with spacer placeholders must be re-entered or restored from a backup.
[0.9.65] — 2026-06-12
Added
- Editor Permissions are now editable (super-admin only). The Add-Ons → PageCraft → Permissions page was display-only — it showed each role's editing level but nothing could ever write the
editor_permissionssetting, so every role defaulted to Full with no way to change it (the limited "Content" level was effectively unreachable). The page now renders a Full/Content selector per role and saves via a newsave_permissionshandler. Setting a role to Content gives that role the limited editor (text editing, file uploads, adding blocks; per-element settings panels hidden). Super Admin (role 1) is always Full and the Banned/Guests/Pending roles are never shown. Non-super-admins see the read-only badges. Only the non-defaultcontentlevel is persisted, so unset roles continue to default to Full.
[0.9.64] — 2026-06-12
Security
- Template Ingest is now super-admin only. It extracts
js/css/svgassets and registers<script>/<link>tags into the editor canvas that load for all users — executable content that can't be meaningfully sanitized — so it's restricted to fully-trusted admins (defense-in-depth above the CP-access gate the other actions use). The "Ingest Template (.zip)" button is hidden for non-super-admins. Pack Import is unchanged (CP-access): it serves CSS only, never JS, and doesn't register site-wide canvas assets. - Uploaded/extracted SVGs are screened for active content. A new shared
Traits/SvgSafety::isSafeSvg()rejects SVGs containing<script>,<foreignObject>, DOCTYPE/ENTITY declarations, inline event handlers (onload=…),javascript:URLs, or external<use>/<image>references. Applied to the Template Ingest preview image (replacing the old bare<svgsubstring check that read only the first 1 KB) and to SVG assets extracted from both Template Ingest and Pack Import zips.
Fixed
- Plugin assets no longer cache-bust on every request. The front-end runtime appended
?v=<timestamp>to every ContentBox plugin JS/CSS URL, defeating browser/CDN caching on each page load. It now uses the add-on version (?v=0.9.64), so assets cache between releases and refetch after an update. The field-tag render path (ft.page_craft.php), which previously had no cache-buster at all, now uses the same version stamp.
Changed
- The bundled-plugin list is now a single source of truth.
ft.page_craft.phpandmod.page_craft.phpeach carried their own copy of the ContentBox plugin list and had already drifted (the field-tag copy was missingtemplate-embed). Both now readThemeAssets::contentBoxPlugins(), so the registerable-plugin set can't diverge between the two render paths.
[0.9.63] — 2026-06-11
Changed
- Section thumbnails now self-heal at render time; the manual "Fix Missing Thumbnails" button is gone. Previously a missing/cross-install thumbnail showed a broken image (or a
—) until you clicked Fix Missing Thumbnails, which wrote SVG files to disk and rewrote DB rows — and it silently skipped sections with an empty thumbnail and could clobber a valid{file:…}reference that was momentarily unresolvable. NowServices/ThumbnailPlaceholder::src()resolves each thumbnail when it renders: it returns the real image URL when the file exists on this install, otherwise an inlinedata:image/svg+xmlplaceholder labeled with the section name + category. This is applied in the canvas picker JS (SectionFileWriter) and both CP section lists (mcp.page_craft.php), so placeholders appear automatically — no button, no disk writes, no DB mutation, and the empty-thumbnail case is covered too. - Removed the
reconcile_thumbnailsACT endpoint and itsActions/ReconcileThumbnails.phphandler.update()deletes the retired action row fromexp_actionson existing installs. Section import no longer generates on-disk SVG placeholders (the renderer handles it); export still bundles real preview images.
Security
- Folded the earlier thumbnail path-traversal guard into a single
themeRel()helper shared byresolveDiskPath()(export) and the new URL resolver, so the..-rejection is enforced in one place.
[0.9.62] — 2026-06-11
Security
- AI proxy cost/abuse guardrails. Both AI endpoints (
ai_proxy,ai_proxy_stream) forward to OpenRouter on the server's shared API key; they now bound what a single CP user can spend. A new sharedTraits/AiProxyGuardadds: a model allowlist (clamped to the models the editor's picker offers — anything else falls back togoogle/gemini-2.5-flash, so a tampered client can't bill an arbitrary/expensive model; extend per-site via$config['pagecraft_ai_allowed_models']), an input cap (max 80 messages and 256 KB of combined content per request → HTTP 413), and a per-member rate limit (40 requests / 60 s, fixed-window via EE cache → HTTP 429). The streaming endpoint no longer echoes the raw cURL error string to the client (logged server-side instead).
[0.9.61] — 2026-06-11
Security
- Control-panel access is now required for every editor, upload, and import action. All state-changing/editor ACT endpoints (
upload,filepicker_bridge,ai_proxy,ai_proxy_stream,editor_frame,template_ingest,pack_import,sections_import,sections_export,reconcile_thumbnails,save_section_from_editor,template_render_preview,structure_nav_preview,template_list) now requirecan_access_cp, not merely a logged-in member. Previously a front-end-only registered member could reach them — most importantly Template Ingest, which writes JS/CSS/SVG assets that are later served on every page. CP editors are unaffected (they always have CP access). The public asset endpoints (custom_css/custom_js/blocks_js) remain intentionally open, and the global-component actions remain super-admin only. - Path-traversal hardening in section-thumbnail resolution.
ThumbnailPlaceholder::resolveDiskPath()now rejects..segments and stays confined to the theme directory. A crafted stored thumbnail URL (e.g.…/themes/user/pagecraft/../../../../etc/passwd) could otherwise be resolved to a file outside the theme root and bundled into the sections export.zip.
Fixed
- "Save as Section" no longer fails on installs that haven't opened the CP Sections page. The
pagecraft_sections.enabledcolumn is now declared in the install schema and backfilled by an idempotentensureSectionEnabledColumn()step inupdate(). It was previously created lazily only when the CP Sections page was first opened, so saving a section straight from the editor (save_section_from_editor) on a fresh install threw "Unknown column 'enabled'". - Canvas-asset upgrade no longer drops a legacy editor stylesheet. The 1.4.0
canvas_assetscreation step now runs before the 1.5.0 split intocanvas_css_assets/canvas_js_assets. On an upgrade that crossed both versions in one pass, the split readcanvas_assetsbefore the legacyeditor_stylesheet_urlwas migrated into it, silently losing the stylesheet.
[0.9.60] — 2026-06-10
Added
- Template Ingest pairs preview images with zips. When ingesting raw-HTML template
.zips (Add-Ons → PageCraft → Sections → Ingest Template), every section previously got an auto-generated SVG placeholder thumbnail. Now you can include a preview image named the same as a zip in the same drop/selection (e.g.hero.zip+hero.png) and PageCraft uses it as that section's thumbnail. Same modal, same flow — just add the images.- Client (
mcp.page_craft.php): the ingest file input accepts images too; dropped images are held in a base-name map and paired to like-named zips. Each zip's upload carries its matched image aspreview_file. The queue shows a "✓ preview" badge on zips that have a match. Images with no matching zip are ignored. - Server (
Actions/TemplateIngest.php::savePreviewImage()): validates the optionalpreview_file(uploaded, image extension, ≤ 5 MB, real-image content sniff including an<svg>check), saves it into the per-slugimported/{slug}/directory, and uses it as the primary (first) section's thumbnail — additional sections from a multi-HTML zip keep the generated SVG. Stored as an absolute pack URL (survives Regenerate File, mirroring Pack Import). Any validation failure silently falls back to the SVG placeholder — a bad preview never blocks the ingest. Preflight is unaffected (the preview is only sent on the real ingest).
- Client (
[0.9.59] — 2026-06-08
Fixed
- Tailwind grid sections now render in the canvas and on the front end. With Grid Framework set to Tailwind, ContentBox emits Tailwind grid classes (
md:w-*/12,flex, …), but nothing loaded a Tailwind stylesheet — the editor canvas loaded only the site's own (Bootstrap/Crafto) CSS, so the Tailwind classes had nothing to style them and grids collapsed. The addon already ships a complete Tailwind build (themes/user/pagecraft/assets/frameworks/tailwindcss/styles.css, with every grid utility CB generates) — it was simply never loaded. Now:- Canvas (
Actions/EditorFrame.php): newframeworkGridCss()/resolveFramework()inject the Tailwind build into the editor<head>(loaded last so its utilities win) when the resolved framework — per-field override falling back to global — istailwind. - Front end (
mod.page_craft.php::enqueueRuntime()): the Tailwind build is added topagecraft_required_csswhen the global Grid Framework istailwind. - The bundled build defines no
.row/.column/.container, so it's purely additive — no collision with ContentBox-native or Bootstrap grids. Bootstrap needs no equivalent (the site's own Bootstrap CSS is already a canvas asset / loaded by the wrapper). - Known scope: the front-end load reads the global framework setting; per-field Tailwind on the front end would need additional entry/field resolution. The full ~260 KB build ships on every Tailwind front-end page (a purged subset is a possible later optimization).
- Canvas (
[0.9.58] — 2026-06-05
Added
- Grid Framework setting (global + per-field). A new setting tells ContentBox which CSS-framework grid vocabulary to drive in the editor canvas: Native (ContentBox's own
.columngrid, the default), Bootstrap (col-md-*), or Tailwind (md:w-*/12). When set, CB operates select/move/resize natively on the framework's own column classes, so imported templates built in that framework get real column tooling instead of a class-name collision. Global default with a per-field override (reuses the existingfield ?: globalfallback pattern). Defaults to Native, so existing installs are unchanged. Wired inmcp.page_craft.php(global),ft.page_craft.php(per-field), andActions/EditorFrame.php(canvas init). - Grid-conversion repair pass (
pcFixGridMath). ContentBox's framework conversion measures each column's width and rounds to the nearest grid unit, which can over-assign on auto-fill (equal, no explicit width) columns — e.g. four flex columns each becomingcol-md-4(16/12) and overflowing the row. A canvas post-process detects rows whose columns' widths sum past 100% and rewrites them to the framework's auto-equal class (colfor Bootstrap,md:flex-1for Tailwind), which never overflows regardless of count. Legitimate grids (sums ≤ 100%, including Tailwind'sw-full md:w-N/12mobile+desktop pairs) are left untouched. Runs on canvas start and before each save.
Documentation
- New "Grid Framework" guide (
docs/07-grid-framework.md, registered in the in-CP docs index). Explains the three options, what "driving the grid" means, and the important caveats: the framework is canvas-wide (not per-section), native sections get converted (lossily for non-12-column layouts), avoid mixing native + framework sections on one page, and the Tailwind purge gotcha. Cross-referenced from "Importing Custom Templates" since that's where a framework user lands after an import.
Tooling
build-release.sh— produces a clean, shippable package: exports the working tree (tracked + new files, honouring.gitignore), forces thepage_craft/addon folder name wrapped inPageCraft-<version>/, strips dev-only files (** contentbox_files **,.git,PAGECRAFT_STATE.md,.claude, OS cruft), and resets the section library to seeded defaults so a developer's test sections/categories/thumbnails never ship. Outputsdist/PageCraft-<version>.zip.dist/is gitignored.
Note
- The framework conversion is inherently lossy for layouts not authored in the target framework's grid; the repair pass is a safety net, not a perfect translator. The intended long-term model is per-framework block libraries (blocks authored natively in each supported framework) with the conversion repair as fallback — see project notes.
[0.9.57] — 2026-06-04
Fixed
- Imported Bootstrap/Crafto grids no longer collapse on the front end or canvas. ContentBox's runtime CSS claims the
.rowclass and, via higher-specificity rules (.is-container .row > *{flex-shrink:initial},.is-container .row{flex-wrap:initial},.row{gap:2rem;margin:0 -1rem}), overrode the theme's Bootstrap grid — collapsingcol-*cells, jammingrow-cols-*cards into a single nowrap row, and applying wrong gutters.ThemeAssets::gridCompatCss()now restores Bootstrap's intendedflex-wrap:wrap/flex-shrink:0/ gutter behaviour, scoped via:has()to rows that actually contain Bootstrap columns (col,col-*, or arow-cols*row), so ContentBox-native.row > .columnlayouts are untouched. - Grid-compat scoping no longer clobbers native ContentBox sections. The column detection matched real Bootstrap tokens via anchored attribute selectors (
[class^="col-"],[class*=" col-"],[class="col"], etc.) instead of a substring[class*="col-"], which wrongly matched theflex-colutility that stock CB columns carry (column flex flex-col …) and broke out-of-the-box layouts.
Changed
- PageCraft field columns auto-upgrade to MEDIUMTEXT. The fieldtype declares
mediumtextininstall(), but EE only applies that at field-creation time — fields created earlier kept a 64KBTEXTcolumn and threw "Data too long for column" (SQLSTATE 22001) when saving a large page. The updater (upd.page_craft.php::ensureFieldColumnsAreMediumtext()) now detects anypage_craftfield whose data column is stillTEXTandALTERs it toMEDIUMTEXT. Idempotent; handles EE 7 per-field tables and the legacy shared table. Mirrors the existingensureAllActionsRegistered()self-healing pattern.
Internal
- Groundwork for adopting imported Bootstrap
col-*cells into ContentBox's column tooling (selection/settings via.column+data-protected) is present inTemplateIngestbehind a disabled$adoptBootstrapColsflag, parked until the grid-layout work above is proven stable.
[0.9.56] — 2026-06-03
Fixed
- Grid-compat CSS now ships on the
{exp:page_craft:render}module render path. The Bootstrap.row/.columncollision fix (ThemeAssets::gridCompatCss(), added in 0.9.54) was emitted by the canvas (EditorFrame.php) and the field tag (ft.page_craft.php), but not by the module tag inmod.page_craft.php— so pages rendered with{exp:page_craft:render …}got ContentBox's.row{flex-wrap:initial}+.row>*{flex-shrink:initial}with no counter, crushing imported Bootstrap columns (col-lg-6, etc.) into a single nowrap row with one-or-two-words-per-line text on the front end while the canvas looked correct.enqueueRuntime()now injectsgridCompatCss()once on first runtime enqueue — the same first-run gate that loads the collidingcontentbox-runtime.css— so the counter-CSS always ships when the runtime does, and never bloats pages that don't load it. Fixesrender(),global_header(), andglobal_footer()in one place. Requiresruntime="yes"on the render tag (the default).
Added
- Self-healing section thumbnails. New
Services/ThumbnailPlaceholdercentralises the labeled-SVG placeholder generator (extracted from the Template Ingest flow) plus host-agnostic broken-thumbnail detection: a thumbnail is "broken" when the file it points to is missing on this install, whether stored as a bare filename or an absolute URL baked to another install's domain (matched on the/themes/user/pagecraft/path marker, not the host).reconcile_thumbnailsaction + "Fix Missing Thumbnails" button (CP → PageCraft → Sections). Scans every section, regenerates a placeholder only for the broken ones, updates the rows, and resyncspagecraft-sections.js. Idempotent and install-agnostic — run it wherever the broken tiles are.- Section import self-heals.
SectionsImportnow accepts a.zip(manifest + bundledpreview/files) or legacy raw JSON, copies bundled previews into place, and auto-generates a placeholder for any section whose image is still missing — so an import never leaves broken tiles. Reportsplaceholders_createdin the result. - Section export bundles images.
SectionsExportnow produces a self-contained.zip(manifest.json + realpreview/files) so thumbnails survive the round-trip; falls back to plain JSON whenZipArchiveis unavailable.
Documentation
- Recommended EE session settings.
docs/00-installation.mdnow explains the "This form has expired" error editors can hit when the publish form sits open during a long canvas edit, and recommends EE's Session type → "Cookies and session ID" (and, only if needed, loosening "Require IP for posting"). The addon does not change these settings itself — they're documented as the site owner's call.
Changed
- Section-import form reports inline instead of dumping JSON. The import
<form>previously did a native POST and left the browser on the raw JSON action response. It now submits via XHR and shows "N sections imported, N categories created, N placeholder thumbnails generated" inline, then reloads — matching the.zipPack Import UX. The file input accepts.zipand.json. - Documentation: template examples + render tag reference. Added
docs/14-template-examples.md(minimal → URL-routed → mixed → full-site → multi-field templates) and documented the previously-undocumented{exp:page_craft:render}tag indocs/13-template-tags-reference.md, including the diagnostic-comment table and the.is-wrapper/wrapper="yes"requirement. Both registered in the in-CP docs index. CP doc code blocks now wrap and grow to content height instead of clipping to one horizontal-scrolling line.
[0.9.55] — 2026-05-31
Fixed
- Multi-column Bootstrap rows now wrap instead of overflowing. 0.9.54's grid-compat fix stopped the column crush but exposed a second collision:
assets/minimalist-blocks/content.csssets.row{flex-wrap:initial}(= nowrap), so a row of fourcol-md-6form fields laid out in one horizontal strip and overflowed the canvas rather than forming a 2×2 grid.ThemeAssets::gridCompatCss()now also forcesflex-wrap:wrapon rows that contain Bootstrap columns, restoring the intended multi-row wrapping. Still scoped via:has()to rows withcol/col-*children, so ContentBox-native.row > .columnlayouts are untouched.
[0.9.54] — 2026-05-31
Fixed
- Imported Bootstrap columns no longer collapse in the canvas or front-end. The real cause of the "one character per line" column crush (not the
.columninjection removed in 0.9.53) is a class-name collision: ContentBox's runtime CSS claims the.row/.columnnames that Bootstrap themes also use, and its.row>*{flex-shrink:initial}overrides Bootstrap's deliberate.row>*{flex-shrink:0}. Next to a fixed-width sibling (e.g. the green-energy 418×550 image, or the contact-form field grid) the text columns shrank toward zero; CB's added.row{gap:2rem}also pushed columns past 100% so they wrapped. A newThemeAssets::gridCompatCss()emits a small compatibility stylesheet —flex-shrink:0andgap:0for rows that actually contain Bootstrap columns (.row > [class*="col-"]/[class~="col"], via:has()), leaving ContentBox-native.row > .columnlayouts untouched. It's injected into the editor canvas (Actions/EditorFrame.php) and the front-end runtime (ft.page_craft.php). Because this is pure CSS keyed on the existing markup, sections ingested earlier are fixed in place — no re-ingest required for the layout (the 0.9.53 re-ingest note applies only to stripping the stale.columnclass).
[0.9.53] — 2026-05-30
Fixed
- Ingest no longer butchers Bootstrap grid layouts. v0.9.51's column recognition added ContentBox's
.columnclass onto Bootstrapcol-*elements, but CB's runtime CSS gives.columnwidth:100%+ horizontal padding while.row{display:flex}drops Bootstrap'sflex-wrap. That overrode the theme'sflex: 0 0 N%and collapsed multi-column sub-grids to content width — contact-form fields and stat columns rendered as one-character-per-line vertical strips. The column-injection step is removed entirely: imported Bootstrap grids are left exactly as the theme authored them (they lay out correctly from the theme CSS registered as a canvas asset, and their content stays editable via theis-subblock editmarkers). The trade-off is that ContentBox's native column-drag handles don't appear on imported grids — correct rendering is the priority. Section,w-100, container, loose-text, and button injection are unchanged. Note: sections ingested under v0.9.51–0.9.52 already have the bad.columnbaked into their stored HTML (and into any page where they were placed); re-ingest them, or run a cleanup, to recover the layout.
[0.9.52] — 2026-05-30
Added
- Bulk actions on the Sections list (CP → PageCraft → Sections). Each row now has a checkbox, with a "select all" checkbox in the table header (including an indeterminate state for partial selections). A toolbar above the table shows the live selected count and offers two group actions: Delete Selected (with a confirmation naming the count) and Assign Category — a "Move to category…" dropdown of existing categories that re-files every selected section in one shot. Both submit to a new
bulk_sections()settings controller that constrains operations to the current site's sections, regeneratespagecraft-sections.jsviaSectionFileWriteronce per batch, and shows a result banner. The bulk Delete Selected button reuses the existing red.pc-delete-btnstyling; the per-row delete handler was scoped to.pc-delete-btn[data-url]so it no longer binds the bulk button. Assign is disabled until both a category is chosen and at least one row is selected; delete is disabled until at least one row is selected.
[0.9.51] — 2026-05-30
Changed
- Template ingest now injects ContentBox classes with a DOM tree pass instead of single-shot regexes.
Actions/TemplateIngest.php::prepareSection()was a chain of regexes (injectSectionClasses,insertOverlay,injectContainerClasses,addEditableMarkers) that each only touched the first matching tag — so real ThemeForest/Crafto markup came through half-wired. It now parses each section fragment into aDOMDocument(loaded under a singledata-pc-rootwrapper withLIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTDand a UTF-8 hint) and injects by traversal. The original regex pipeline is preserved verbatim asprepareSectionRegex()and runs automatically as a fallback whenever a fragment can't be parsed or the DOM pass yields nothing, so hard-to-parse files are never worse off than before. Inline<script>/<style>bodies and complex attributes (e.g.data-anime='{…}') survive the round-trip intact.
Added
- Grouped sections. A file containing multiple top-level
<section>elements now gets every section wired up — each receivesis-section is-box is-section-auto w-100and its own<div class="is-overlay">first child — not just the first. Files with no<section>tags are treated as one section per top-level block element (a single wrapping<div>, or sibling block divs); flat snippets (loose<h2>/<p>/inline content) defer to the regex fallback rather than being wrapped in bogus sections. w-100on every section alongside the existingis-section is-box is-section-auto.- Bootstrap container recognition. Elements classed
container,container-fluid, orcontainer-{sm,md,lg,xl,xxl}(and the legacypc-containermarker) getis-container v2 is-content-1200 is-builder+data-sort="true"appended, keeping their originalcontainer*classes. A section with no recognised container still falls back to promoting its first non-overlay child, as before. - Column recognition. Bootstrap column classes (
col,col-6,col-md-4,col-lg-12,col-auto, responsive barecol-md, …) get ContentBox'scolumnclass appended (originals kept), and their parent gainsrowif missing — so multi-column layouts are sortable/editable as CB columns. - Loose-text editability. Visible text living outside
<p>/<h1>–<h6>is now made editable:<ul>/<ol>containing text become a single editable region (the whole list, not per-<li>), and text-holdingdiv/span/li/figcaption/blockquote/dd/dt/td/th/labelelements getis-subblock edit. A nesting guard prevents marking any element already inside an editable region; structural nodes (is-section/is-container/row/column/is-overlay) are never marked. - Button recognition.
<a>/<button>elements with button-ish classes (btn,button,*-btn,btn-*) or an existingrole="button"getrole="button"set (ContentBox's content-button form), so the button tool wires up on the canvas. Theme classes andhrefare left intact; plain text links are not touched.
[0.9.50] — 2026-05-30
Added
- Target category is now chosen at ingest time. The Ingest Templates modal (CP → PageCraft → Sections → Ingest Template) now has a required Target category dropdown listing existing categories. The Ingest button stays disabled until a category is selected, and every section from every ZIP in the batch is filed under that one category. This replaces the previous behaviour where
Actions/TemplateIngest.phpsilently derived a category from each ZIP's folder structure / filename and auto-created categories on the fly — which scattered an imported theme across a dozen machine-named categories. If no categories exist yet, the modal shows an inline notice linking to the categories page instead of an empty dropdown, since ingest now requires picking an existing one.
Changed
- Ingest is non-destructive except by name collision, and warns first. Existing sections are left untouched unless an incoming section has the same name within the chosen category, in which case the existing row is overwritten (HTML + thumbnail) rather than duplicated. Before any write, clicking Ingest runs a preflight pass — each ZIP is parsed server-side (
mode=preflight, no disk or DB writes) to compute the section names it would produce and which collide with the target category. If any collide, a single consolidated confirmation lists every section that will be overwritten (with its source file) and the user can cancel the whole batch, leaving everything unchanged. On confirm, the real ingest runs withoverwrite=1and upserts by(category_id, name). Server contract: withoutoverwrite=1, a colliding section is skipped (reported inerrors) rather than overwritten, so direct API callers don't clobber data without opting in. Response JSON gainsoverwritten,skipped, andcategory;categories_createdis removed (no categories are created during ingest anymore).
[0.9.49] — 2026-05-30
Added
- Multi-file template ingest. The Ingest Templates modal (CP → PageCraft → Sections → Ingest Template) now accepts multiple
.zipfiles in a single drop or file-pick. Each ZIP is queued, processed sequentially as its own pack (one request per file), and reports per-row success/error inline. Sequential rather than parallel becauseSectionFileWriter::write()rewritespagecraft-sections.jsafter every ingest — concurrent writes would race on the same file and onsort_orderallocation inexp_pagecraft_sections. Failures are isolated per row; a batch with mixed outcomes leaves the modal open so editors can review what failed before navigating away (all-success keeps the existing auto-redirect). Files can be removed from the queue before submit; duplicate adds (same name + size) are de-duped automatically. Non-.zipfiles dropped into the zone are silently ignored with a one-line notice. Server-sideActions/TemplateIngest.phpis unchanged — it still expects onetemplate_fileper request, which is exactly what each row sends.
Fixed
- Editor frame no longer crashes when a
template_packssetting slug points to a missing on-disk pack. Previously, the file-not-found branch inActions/EditorFrame.php(added so a missing pack wouldn't cascade-break the templates panel) calledee()->logger->developer()without first loading theloggerlibrary —__get('logger')then threwInvalidArgumentExceptionand the whole editor frame failed to render. The library is now loaded immediately before the call, and the entry is flagged "always log" so it appears in Tools → Logs → Developer Log regardless of debug level. Exposed by deleting the bundledcrafto-corporate,crafto-elements,crafto-premiumtemplate-pack directories without first un-checking them in the Template Packs settings.
[0.9.48] — 2026-05-30
Added
- Typography panel toggle. New General-settings switch (
Enable Typography Panel, default on) that hides ContentBox.js's sidebar Typography panel and its toolbar button when disabled. The Typography panel is section-scoped by design — editors picking different presets per section is a common source of inconsistent typography across blocks. When the toggle is off, the existingCustom CSSsetting is the intended driver (loaded into the canvas via?ACT=custom_cssand into the front-end via the runtime enqueue /{pagecraft_css}placeholder), giving sites a single source of truth for fonts, sizes, and headings. ContentBox does not expose a clean disable API for the panel, so the implementation injects adisplay: none !importantrule for#divSidebarTypographyand[data-command="typography"]into the outer editor frame's<style>block. Existing installs pick up the default on upgrade via an idempotent migration inupd.page_craft.php; this change does not affect the canvas iframe, the rendered front-end output, or any other setting.
[0.9.47] — 2026-05-20
Added
- Parent-entry context tokens for Dynamic Content embeds. Embedded EE templates can now reference the PageCraft entry being rendered via
{pagecraft_entry_id}and{pagecraft_url_title}tokens. Use them in place ofdynamic="yes"(which doesn't resolve reliably inside a fragment parse — symptom:{exp:channel:entries dynamic="yes"}returning all entries instead of the current one). Example:
Tokens for unset keys substitute to empty string, so a literal{exp:channel:entries channel="locations" entry_id="{pagecraft_entry_id}"} ... {/exp:channel:entries}entry_id=""is harmless. Wired through both render paths:ft.page_craft.php::replace_tag()(the in-channel-loop case) andmod.page_craft.php::resolveTemplateEmbeds()(the{exp:page_craft:render}standalone case). Implemented asTemplatePreview::substituteContext()— a static helper both paths share so they can't drift.
[0.9.46] — 2026-05-17
Added
- Dropdown "Distance from Nav" control. New field in the Nav Items modal's DROPDOWN tab, sets the gap between the nav and the submenu when a parent menu item is hovered/focused. Drives
--nav-dd-mtconsumed by.submenu { margin-top: var(--nav-dd-mt, 0.5rem) }. Range 0–5 (same scale as the other margin controls), stored asdata-nav-dd-mton the plugin element.
Changed
- Color swatch + override-checkbox order swapped. v0.9.45 had
[checkbox][swatch]which made the empty checkbox the most prominent element and the swatch easy to miss (especially when the value was white-on-white). Now[swatch][checkbox]— swatch is the primary visual + click target, checkbox is the secondary override toggle on the right. Border bumped from#ccc→#999so the swatch is always findable even on white values.
[0.9.45] — 2026-05-17
Fixed
- Section Background Color now actually paints the nav bar. Root cause exposed by the v0.9.44 diagnostic: stale
data-cb-bg-color="rgba(255, 255, 255, .95)"attributes left over from v0.9.39-and-earlier entries were winning over the section bg via thelegacy || sectionBgpriority. The user could see the section bg detection working perfectly in the console (tracking fromrgb(174, 96, 96)torgb(149, 93, 93)as the picker dragged) but the nav stayed translucent-white because legacy always won. Fix: removed the legacy priority entirely (the nav-specific bg picker is gone from the UI as of v0.9.40, so there's no longer a reason to honour the old attribute) and stripdata-cb-bg-colorfrom the element on first mount under v0.9.45+ so the stale value doesn't keep leaking back.
Changed
- Color swatches now render as 40×40 px squares matching CB's existing section box swatch size — was a 52×34 rect. Added
position:relative; z-index:1so the swatch sits above any modal-overlay click capture (related to the user's report that the Background Color swatch was harder to click than the Text Color swatch — both should now be equally hittable).
[0.9.44] — 2026-05-17
Changed
- Toolbar reverted to light pill (white background, dark icons, soft border + shadow) matching existing CB UI patterns. v0.9.43's dark pill was a misread of the body content toolbar's actual styling.
- Color swatches reverted to native HTML5
<input type="color">at a fixed 52px width. v0.9.43's CB-openColorPicker-backed button rendered as a thin sliver because the integration from plugin context didn't reliably mount the Pickr popover. Native picker is reliable across all browsers; brand-color swatch support is deferred to a separate follow-up. Swatch is now visibly clickable (52×34 px with a 1px border and 2px inner padding so the colour swatch reads).
Fixed (in progress)
- Section Background Color reaching the nav — still being debugged. v0.9.44 widens detection to probe the section itself AND its
.is-overlay/.is-overlay-content/.is-boxdescendants for inlinestyle.backgroundColorand the various data-attr keys CB might use, and tightens the MutationObserver to watch attribute changes on the section's full subtree (was section-only). Adds a[pc-nav] applyNavBgconsole.debugentry to the canvas console so the inputs to the picker decision are visible — please open canvas DevTools → Console and paste a sample log line after picking a section bg colour so we can identify the exact CB persistence path on this install.
[0.9.43] — 2026-05-17
Changed
- Per-item toolbar restyled to match CB's body content edit toolbar — dark pill shape with white icons (was light rounded-rect with dark icons). Border-radius bumped to fully round; background
#1a1a1a; icon hover is a subtle white-alpha highlight. - Color swatches now render at proper width. v0.9.42's
flex:1color input collapsed to a thin vertical line inside the row2 grid becausemin-widthdefaulted to "auto" content size. Addedflex:1 1 auto; min-width:0; width:100%;on the swatch andwidth:100%; box-sizing:border-box;on the row wrapper so the swatch fills its grid cell. - Color pickers now use ContentBox's
openColorPicker(Pickr) — shows the configured brand-color swatches + hue + alpha + rgba text input, matching the section box settings picker the user referenced. Native<input type="color">remains as a fallback if the CB API is unavailable. Both the Nav Items modal and the nav-button modal use the same widget. - Nav CTA button modal now tabbed (GENERAL / HOVER / TEXT) matching the Nav Items modal pattern. Label / URL / Style / New Tab stay at the top above the tab bar — they're button-specific, not styling. HOVER tab adds Background Color, Text Color (Dark / Light / Current), Border Style. TEXT tab adds Font Size, Upper/Lower (B / Aa / U), Letter Spacing. Hover styling persists via a
<style class="pc-nav-btn-hover-styles">element inside the nav plugin, with each button getting a stabledata-btn-idand a per-button:hoverrule scoped to that ID. - New DROPDOWN tab on the Nav Items modal — controls for the submenu panel: Background Color, Text Color, Hover Background, Hover Text Color, Dropdown Radius, Item Padding L/R + T/B. Wires through
--nav-dd-*CSS variables consumed by.submenuand.submenu-linkrules instyle.css.
Fixed
- Section's Background Color picker now reliably paints the nav bar. v0.9.41's section-bg read checked
style.backgroundColoranddata-cb-coloronly; if CB writes the section's bg under a different attribute name the nav never picked it up. Added fallbacks fordata-bg-color,data-bg,data-color, and a finalgetComputedStyle(section).backgroundColorcatch-all that handles any CSS-rule-driven background. Widened the MutationObserver to watch ALL section attribute changes (was filtered tostyle+data-cb-color) so any picker write triggers a repaint.
Internal
- New
buildBtnHoverRule(b)+refreshBtnHoverStyles()helpers inopenContentEditor. Hover CSS rules are emitted as[data-cb-type="nav-menu"] .nav-btn[data-btn-id="..."]:hover { ... }so they're scoped per-button and survive<style>-element HTML round-trip. readNavLinkGlobal/applyNavLinkGlobalextended with eight new dropdown keys:ddBg,ddColor,ddHoverBg,ddHoverColor,ddRadius,ddPaddingX,ddPaddingTop,ddPaddingBottom. Stored asdata-nav-dd-*attributes on the plugin element; CSS variables--nav-dd-*consumed by.submenu/.submenu-linkinstyle.css. mount() re-applies on every canvas init so values persist across reloads.
[0.9.42] — 2026-05-17
Changed
- Nav-link edit modal redesigned to match CB Button modal pattern + scoped GLOBALLY. Per user direction: clicking any
.nav-linkor.submenu-linkin the canvas → toolbar → Edit now opens a tabbedNav Itemsmodal with three tabs (GENERAL/HOVER/TEXT) mirroring CB's Button modal layout. SAVED and TEMPLATES tabs are intentionally omitted. Changes apply to every nav-link in the nav instance, not the clicked one — settings live asdata-nav-link-*attributes on the plugin element and drive.nav-linkCSS via inline CSS variables. The earlier per-link modal (v0.8.0–v0.9.41) is replaced.- GENERAL tab: Background Color, Text Color, Width, Border Style, Border Thickness, Padding Left/Right, Padding Top, Padding Bottom, Border Radius, Margin Left/Right/Top/Bottom.
- HOVER tab: Background Color, Text Color (Dark / Light / Current), Border Style.
- TEXT tab: Font Size (12–32 px presets), Upper/Lower (B / Aa / U), Letter Spacing (0–200 with 25-unit steps).
- Per-item style controls removed from the slideout menu-items editor. The old "Item Style" subpanel (Background / Text / Weight per row) was redundant with the global modal and led to drift between the two surfaces. Slideout rows now show only label / URL / new-tab / delete + add-submenu controls; all visual styling moved to the canvas-side modal.
- Per-item style attributes no longer written into nav-link HTML by
updateMenu(). Markup stays clean; styling is global via CSS variables on the plugin wrapper.
Internal
- New helpers in
openContentEditor:readNavLinkGlobal(),applyNavLinkGlobal(state). Both operate onelement.dataset.navLink*keys, not on individual<a>elements.mount()also reads those keys and re-emits the CSS variables on every canvas init so values render correctly on reload. themes/user/pagecraft/assets/plugins/nav-menu/style.css:.nav-linkrules refactored to read from--nav-link-*CSS variables with sensible fallbacks. New rule for[data-nav-link-width="true"] .nav-link { width: 100%; display: block }.- Legacy per-item
data-nav-bg/data-nav-color/data-nav-weightattributes on individual<a>elements are no longer written, but old entries that still carry them are unaffected — there's no read-side that consumes them now, so they render with whatever the global CSS variables produce.
[0.9.41] — 2026-05-17
Fixed
- Per-item nav-link toolbar still didn't fire after v0.9.40's capture-phase click handler. ContentBox uses
mousedown(notclick) for its selection chain, and likely callspreventDefault()/stopImmediatePropagation()on the mousedown that would have otherwise generated a click — so our click-capture listener never fired. Added amousedown-capture handler in parallel with the click handler on the canvas document. Mousedown reaches the document before any descendant handler can suppress it. Also added a[pc-nav-menu]console.log so the canvas console shows whether the handler fires and how many.nav-btn/.nav-link/.submenu-linkelements were detected on mount — useful for further diagnosis if needed. - Source segmented toggle stopped working after the first click. CB's settings form re-renders on every
inputevent (seegenerateFormincontentbox.esm.js), which replaces the rendered<select>we'd upgraded with a fresh one — and our v0.9.40 one-shot upgrade scan didn't catch the new select. Replaced the bounded setTimeout sweep with a persistentMutationObserveron the parent document body that re-runsscanAndUpgradeon every DOM mutation. Idempotent viaselect.dataset.pcUpgradedso already-upgraded selects skip cleanly. - Section's Background Color now paints the nav bar. v0.9.40 made
.nav-wrappertransparent so the section bg "shows through" — but the nav isposition: sticky, so once the user scrolls past the section height the section bg scrolls out and the nav becomes truly transparent over the page content. Nowmount()reads the section'sstyle.backgroundColor(ordata-cb-colorfallback) and applies it directly to.nav-wrapperso the nav stays painted regardless of scroll. AMutationObserveron the section'sstyleanddata-cb-colorattributes keeps the nav in sync when the editor changes the section's bg picker live, no save+reload needed.
Changed
- Source toggle labels shortened to
Manual/Structure(were "Native (manual menu items)" / "Structure (EE page tree)"). Fits the segmented button widths cleanly and matches the user's verbal naming.
[0.9.40] — 2026-05-17
Fixed
Uncaught TypeError: Cannot read properties of undefined (reading 'cloneNode')fromsendSave. The periodic 2-second save tick could fire before ContentBox finished initialising the canvas iframe and set its internalwrapperElreference. CB'shtml()doesthis.wrapperEl.cloneNode(true)as its first line and crashes when wrapperEl is undefined. GuardedsendSaveto skip cleanly whenbuilder.wrapperElis missing, and wrapped thebuilder.html()call in a try/catch so any future races log + skip instead of flooding the console. The next tick (or onChange) catches the actual content once init completes.- Per-item nav-link / submenu-link toolbar still didn't appear after v0.9.39's delegated-handler fix. Binding to the nav wrapper bubble-phase wasn't enough — ContentBox's own click chain was stopping propagation higher up before our handler fired. Re-bound to the canvas DOCUMENT in CAPTURE phase, which fires before any descendant capture or bubble. Now reliably surfaces the Edit / Delete toolbar on click of any
.nav-linkor.submenu-link— including links injected later byapplyStructureItems()in Structure mode.
Changed
- Removed the nav-specific Background Colour setting entirely. Was producing a second color picker in the slideout that duplicated the section box's existing Background Color (already at the top of the slideout via CB's section settings). Confusing — both pickers drove the same visual. Now the nav bar inherits the parent section's background color directly. The
.nav-wrapperCSS defaults to transparent so the section's bg shows through. Legacy entries that still havedata-cb-bg-colorsaved are honoured at mount time for backwards compatibility — the inlinebackgroundoverride is applied so the visual doesn't change. New entries simply use the section picker. - Source and Width settings now render as segmented two-button toggles matching the visual style of CB's Responsive panel's Visible | Hidden control. Implementation: kept the underlying
type: 'select'declarations so CB's save round-trip still works through the native<select>element, then upgrade the rendered DOM inopenContentEditor— hide the select, insert a segmented button widget next to it, wire button clicks back toselect.value + dispatchEvent('input')so CB's form listener picks up the change. Idempotent viaselect.dataset.pcUpgraded, polled at 0 / 60 / 250 / 800 ms to catch the settings panel render whenever it happens. The select stays in the DOM (display:none) as the source of truth.
[0.9.39] — 2026-05-17
Fixed
- Nav-menu per-item edit toolbar regression. Clicking a
.nav-linkor.submenu-linkin the editor canvas now reliably surfaces the floating Edit / Delete toolbar (the modal opens with the full visual field set — Background / Text / Font weight / Font size / Width / Border style/thickness/radius / Padding L/R + T/B / Margins L/R/T/B). Cause: the per-element click handlers at the end ofmount()only bound to elements that existed at mount time — whenapplyStructureItems()(Structure mode) or any other in-place DOM rewrite replaced the.nav-listinnerHTML, the new.nav-linkelements had no listener and the toolbar silently failed to open. Replaced with a single delegated click handler on the nav wrapper usingelement.closest('.nav-link, .submenu-link'), with previous-handler cleanup on re-mount so the latest mount'sshowToolbarclosure wins (no stale references to detached toolbar elements).
Changed
- Slideout settings panel no longer carries per-button styling controls. The Customize → BG Color / Text Color / Border / Padding / Margin / Border Radius / Width block that lived inside each CTA button's slideout row (lines 1278–1431 of
themes/user/pagecraft/assets/plugins/nav-menu/index.js) is removed. The same field set lives on the canvas-side click→toolbar→openBtnModalpath, which has room for the full UI without the slideout's real-estate constraints. The two surfaces were drifting; cutting the slideout copy resolves it. Slideout button rows now show only label / URL / style / new-tab + add/delete/reorder. - Nav-bar Background Colour is now a proper color picker. Was
type: 'text'(hand-type#ffforrgba(...)); nowtype: 'color', which renders ContentBox's Pickr-style picker with hue + alpha slider + swatches + rgba text output — the same widget Section box settings use. Existing saved values (hex or rgba) round-trip unchanged. - Layout dropdown: Side Nav removed. The fixed-left-sidebar variant was lightly finished and crowding the dropdown. Removed from the option list, removed from
style.css(~80 lines of.nav-side-variant/.variant-side-navrules), andmount()now migrates any savedvariant=side-naventries toleft-navautomatically so existing entries don't break. - Source and Width settings converted to dropdowns. "Use Structure to Generate Navigation" was a checkbox → now
Sourcewith optionsNative (manual menu items)andStructure (EE page tree). "Full width" was atype: 'toggle'rendering as "Full width / true" plain text → nowWidthwith optionsBoxedandFull Width. Underlying data round-trip is unchanged; this is purely a clearer UI label and explicit two-state semantics. (Note: true segmented two-button-group rendering — matching the Responsive panel's Visible / Hidden control — needs a CB field-type addition we don't have; deferred.)
Internal
- No DB migration. Saved entries from v0.9.38 and earlier render unchanged.
- All four nav-menu items live in a single file —
themes/user/pagecraft/assets/plugins/nav-menu/index.js. Repo's bundled copy (system/user/addons/page_craft/themes/user/pagecraft/assets/plugins/nav-menu/) synced from the deployed local copy.
[0.9.38] — 2026-05-16
Added
- Copy/Paste Phase 3 — style paint-brush. Right-click any element / block / column / section → menu now includes "Copy styles (kind)" which captures only the target's inline
styleattribute + class list (no inner HTML, no CSS-rule extraction — just the visual properties applied to that one element). The clipboard stores it askind="styles"with asourceKindfield. Right-click another element of the same kind and pick "Paste styles (to kind)" to apply the inline style + classes wholesale, like Word's Format Painter or Figma's "Copy properties → Paste here." - Marker-class preservation. Pasting styles onto an element does NOT clobber the CB-required marker classes for the target's kind:
is-subblock editfor elements,columnfor columns,is-section is-boxfor sections. After the class swap we re-add any missing markers so CB still recognises the element after paste. Block-kind has no class marker (identity comes fromdata-cb-type) so its full class list is replaceable. - Same-kind restriction. Style paste only enables when the right-click target has an ancestor of the same kind as the source. Element styles can only paste onto an element; column styles can only paste onto a column. Prevents the obvious foot-gun of pasting section styles onto a paragraph and losing the paragraph's CB markers.
- What travels with styles: inline
styleattribute +classattribute. Does NOT travel: rule-based styling that lives inmainCss/sectionCssfor non-class selectors. If your styled element relies on entry-wide CSS rules that match by tag or attribute, those don't come withCopy styles— useCopy element(Phase 2) instead, which walks and brings matching CSS rules.
[0.9.37] — 2026-05-16
Added
- Copy/Paste Phase 2 — columns, blocks, and elements (right-click menu). The right-click clipboard now works at every level of the ContentBox hierarchy, not just sections:
.is-subblock.edit— text/image elements (paragraphs, headings, individual images)[data-cb-type]plugin blocks (nav-menu, swiper-slider, accordion, …).columninside a CB.row— columns.is-section.is-box— full sections (already shipped in v0.9.34)
- The resolver now walks ALL ancestors of the right-click target, not just the deepest match. Right-clicking inside a paragraph that lives inside a column inside a section shows Copy element / Copy column / Copy section stacked top-to-bottom — pick the granularity you actually want, no need to right-click on an empty edge of a wrapper. Paste options only appear when the clipboard's kind matches a found ancestor, so you can't accidentally try to paste a section inside a paragraph. When the clipboard contains a non-matching kind, the menu shows a disabled "Clipboard: section (no match here)" hint so you know the clipboard isn't empty, it's just incompatible with the right-click target.
- Pastes for all four kinds use the same code path — generic
copyUnit(kind, el)andpasteAdjacent(clip, target, where)replaced the section-specific helpers. CSS-rule walking, sectionCss merge with dedupe, andbuilder.applyBehaviorOnre-decoration (so toolbars/handles appear immediately) all flow through one implementation.
Internal
- Replaced the v0.9.34
resolveSelectable(deepest-match) withresolveSelectables(all-ancestors).buildContextMenuItems(found, clip)assembles the menu dynamically per right-click. OldcopySection/pasteSectionconsolidated intocopyUnit/pasteAdjacent. ~70 net new lines inActions/EditorFrame.php.
[0.9.36] — 2026-05-16
Fixed
- Pasted sections now get their ContentBox toolbar / drag handles / click chain immediately, no save+reload required. v0.9.34's
pasteSectioninserted the cleaned markup viaparentNode.insertBeforebut didn't tell ContentBox about it, so the new section had no.is-section-tooloverlay, no Add/Move/Settings/Delete buttons, and clicking inside it didn't engage CB's editor — until the user saved the page (at which point a freshloadHtmlfrom saved JSON triggered CB's normal section setup pass). Fix: after inserting, callbuilder.applyBehaviorOn(.is-builder)with the same two-pass + 150 ms layout delay pattern that Grid Builder's column split uses (EditorFrame.php::applyBehavioraround line 942). Also firebuilder.opts.onChange()so CB's own debounced save path kicks in alongside our directpagecraft-request-savepostMessage — belt-and-suspenders. Pasted sections are now fully interactive the moment they land.
[0.9.35] — 2026-05-16
Fixed
- v0.9.34's copy/paste IIFE broke the editor canvas due to a PHP-heredoc escape clash. The new paste-merge code contained
'\n'inside JS string literals (to join CSS rules with newlines). The outer editor frame'secho <<<HTML … HTML;heredoc is unquoted, which means PHP processes backslash escape sequences inside the body — so\nin my JS source became an actual newline character in the rendered response, landing inside a JS single-quoted string literal. Browsers reportedUncaught SyntaxError: Invalid or unexpected tokenat the rendered file's column ~55 of that line, ContentBox never finished initializing, and the canvas didn't load. Fix: escape the backslash ('\\n') so PHP renders the literal\ntwo-char sequence into the JS source. v0.9.34's intent (copy/paste sections via right-click) now works as designed. - Lesson encoded for future canvas-side JS changes inside the outer heredoc: any backslash escape inside a JS string literal must be written as
\\in this PHP source so the rendered HTTP response carries the single backslash through to the browser. Same gotcha would bite'\t','\r','\u…', etc.
[0.9.34] — 2026-05-15
Added
- Copy/Paste sections in the editor canvas (right-click menu). Right-click any section in the canvas to get a context menu with Copy section / Paste section above / Paste section below. The clipboard is a single JSON payload stored in
localStorageon the CP origin, so it survives page reload and is shared across browser tabs — copy a section in one entry, navigate to another entry, paste it there. On copy: ContentBox editor-only markup is stripped (.is-section-tool,.is-box-tool,[contenteditable],data-cb-original-content, etc., mirroring the set used byActions/SaveGlobalComponent.php), then the entry'smainCss+sectionCssare walked for rules whose selectors match elements in the copied subtree. Matching rules travel with the markup. On paste: cleaned markup is inserted as a sibling above/below the right-clicked section, matched CSS rules are merged into the destination entry'ssectionCss(de-duped against existing rules), and the canvas requests a save. The pasted section'sdata-cb-typeblocks (sliders, accordions, etc.) re-initialise normally because ContentBox's runtime watches the iframe DOM for new nodes. - This is Phase 1 of the copy/paste system per the approved plan; column / block / element granularity ships in v0.9.35 and the style paint-brush in v0.9.36. v1 deliberately skips
@media,@supports, and@keyframesrules during CSS extraction (top-level rules only) — adequate for the common case where sections are styled flat.
Internal
- All copy/paste logic lives in a self-contained IIFE inside
Actions/EditorFrame.php(added after the existing Save-as-Section block, ~line 1230). No new DB tables, no new ACT endpoints — purely client-side.
[0.9.33] — 2026-05-12
Fixed
- Dynamic Content sections that use animation libraries now show their content in canvas. Editors reported seeing the embedded template's wrapper containers but no content inside — common with Crafto-style themes that use AOS / anime.js /
data-anime/.wowto fade content in on scroll. The cause is that template-embed paints the rendered HTML viaelement.innerHTML = data.html, and per HTML spec<script>tags inserted viainnerHTMLare inert — they parse but never execute. So the animation library never initializes and elements stay at their pre-animation state (opacity: 0, transformed offset). Added a canvas-only CSS shim totemplate-embed/style.cssthat forces visibility on the common hide-until-animate markers ([data-aos],[data-anime],[data-shadow-animation],.fade-in,.wow,.aos-init,.invisible,.opacity-0). The shim is scoped to[data-cb-type="template-embed"], which only exists in the canvas — on the front-endresolveTemplateEmbeds()replaces the section and strips the attribute, so production animations are never affected. Sliders may still not slice/auto-rotate in canvas (that needs the JS to run, which is a separate fix), but at least the content is visible for layout-positioning purposes.
[0.9.32] — 2026-05-12
Fixed
- Swiper-slider new slides actually appear now. v0.9.31 fixed the slide HTML structure but the new slide still rendered blank — and the cause turned out to be Swiper itself, not the markup. The "+ Add Slide" handler was appending the new
<div class="swiper-slide">directly to.swiper-wrapperwithout telling the Swiper instance, so Swiper's internal slide registry never saw it. Because the plugin's CSS keeps.slide-captionatopacity: 0; transform: translateY(40px)until the parent slide gets.swiper-slide-active, and that class is only set by Swiper on slides it knows about, the new slide stayed invisible (image and caption both). Fix: after appending the slide, callelement.swiperInstance.update()andslideTo(lastIndex, 0)so Swiper picks up the new slide and immediately navigates to it — the user sees their new slide and the caption fades in.
[0.9.31] — 2026-05-12
Fixed
- Nav-menu z-index bumped from 1 → 99 (
themes/user/pagecraft/assets/plugins/nav-menu/style.css). The sticky nav was being overlapped by Dynamic Content sections and other content with stacking contexts above 1. Z=99 stays well below modal/toast z-indexes (which the plugin uses at 99997+) while comfortably winning over generic page content. - Swiper-slider "+ Add Slide" no longer drops the placeholder content. The Add-Slide handler in
swiper-slider/index.jsproduced a stripped-down slide (<div class="slide-caption">with a single<h3 class="slide-caption-title">and<p class="slide-caption-description">) that didn't match the structure the seeded slides ship with — nois-container v3on the caption, no eyebrow<p class="uppercase tracking-225 size-12">, no Read More button. New slides now mirror the seed structure so the placeholder layout is preserved. - Swiper-slider hardening against host-theme CSS conflicts. When the host theme (e.g. Crafto) loads its own
swiper-bundle.min.css, several plugin rules lost the cascade fight. Added scoped!importantto:.swiper-paginationcentering (display: flex,justify-content: center,left: 0,right: 0,transform: none) so the dots/bars no longer flush left when a host theme overrides Swiper's defaults.- Nav-button arrow glyphs — every
.swiper-button-next/prev::after(all three style variants: transparent / solid / arrow) now forcesfont-family: swiper-icons, explicitcontent: 'prev'/'next', and visibility. Host themes that null out Swiper's::aftercontent (or swap the font for FontAwesome) no longer hide the arrows. - Hover transitions — baseline
transform: scale(1)on the nav buttons andtransform: scale(1.1) !important(or1.05for solid) on hover, so any host-themetranslateYhover effect can't displace the button. Circles now grow in place instead of jumping down.
Internal
- No version-gated behavior change in PHP — this release is plugin-asset only.
addon.setup.phpversion still bumped so CP shows a tracked release.
[0.9.30] — 2026-05-12
Reverted
- The
.containermax-width override from v0.9.29. Misread the user's intent: the embedded template's.containercap at 1320px is intentional design — the template author chose.container(centered, bounded) rather than.container-fluid(full width) for a reason, and we shouldn't break it. The actual intent was for the section wrapping the container to stretch to full viewport width, with the container then centering content inside that section. The v0.9.28 wrapper-chain neutralizers already make the section full-width by uncapping CB's.is-container.is-content-1200. The 1320px-container-inside-full-width-section pattern is what the template author designed, and now matches.
Internal
- Canvas iframe cache buster bumped:
v=0.6.6→v=0.6.7.
[0.9.29] — 2026-05-12
Changed
- Override the embedded template's own Bootstrap container max-width inside Dynamic Content sections. Inspector showed the canvas constraint wasn't coming from CB's
.is-container.is-content-1200(we already neutralized that) — it was the rendered template's own<div class="container">capping at 1320px (Bootstrap default, plus amax-width: 1320pxfromcustom.cssin the user's theme). Templates use.containerfor intentionally bounded layouts, but inside a Dynamic Content section the user wants the section to span the full available width like a CB native section does. Added an override scoped to.is-section.pc-section-template-embed(canvas) and.pc-dynamic-render(front-end) that setsmax-width: none !importanton.container,.container-sm/-md/-lg/-xl/-xxl. Front-end may also become wider as a side effect of this — if that's undesired, scope the rule to canvas-only by removing the.pc-dynamic-renderselectors.
Internal
- Canvas iframe cache buster bumped:
v=0.6.5→v=0.6.6.
[0.9.28] — 2026-05-11
Fixed
- Same Bootstrap-stacking bug as 0.9.27, but on the canvas side. The canvas-only neutralizer rules in
template-embed/style.cssfor.is-section.pc-section-template-embed .rowand.columnused descendant combinators, so they matched not only ContentBox's own.row.clearfix > .column(which we wanted to neutralize) but ALSO Bootstrap's.row > .col-*deep inside the rendered template — forcing every Bootstrap column to width:100% with the sameflex: 1 1 100% !important; margin: 0 !important; gap: 0 !importantreset that broke the front-end. Effect in canvas: even when the container max-width was uncapped, the rendered template's columns stacked vertically. - Fix: changed both rules to child combinators (
>) so they reach only CB's immediate row and column inside the section's container —.is-section.pc-section-template-embed > .is-container > .row > .column. Bootstrap rows nested inside[data-cb-type="template-embed"]are at a different depth and don't match the child-combinator chain, so their layout is preserved.
Internal
- Canvas iframe cache buster bumped:
v=0.6.4→v=0.6.5so existing canvas tabs pick up the updated rules.
[0.9.27] — 2026-05-11
Fixed
- Bootstrap responsive columns inside rendered Dynamic Content sections were forced to single-column mobile-mode layout at every viewport size. Diagnosis via DevTools showed
.pc-dynamic-renderwas correctly at full viewport width (2182px on a 2455px-wide canvas), but the columns inside still stacked. Root cause: an over-eager rule from v0.9.22 —.pc-dynamic-render .row > * { width: 100%; max-width: 100%; ... }— had specificity (0,2,0) and beat Bootstrap's responsive.col-lg-X { width: 41.66% }rules at (0,1,0), forcing every column to 100% width regardless of viewport. The rule was originally added to re-emit Bootstrap's default.row > *padding "for safety," but it was both unnecessary (CB's.columnselector doesn't match Bootstrap's.col-*, so there's nothing to restore) AND harmful. - Fix: removed the
.pc-dynamic-render .row > *block entirely. Bootstrap's native.row > *rule (specificity 0,1,1) still provides default padding/width for any non-col-* child, and the responsive.col-*rules now win for col-* children. The.pc-dynamic-render .rowrule (gutter margins) stays — that one is genuinely needed to undo CB's.row { gap: 2rem; margin: 0 -1rem }clobber.
[0.9.26] — 2026-05-11
Fixed
- template-embed plugin CSS was never being fetched by the browser even though
enqueueRuntimecorrectly registered it in the runtime script'snew ContentBoxRuntime({ plugins: { ... } })block. Root cause: ContentBoxRuntime's plugin loader is lazy — it only fetches a plugin's CSS/JS when it finds a matching[data-cb-type="..."]element in the DOM at init time. AfterresolveTemplateEmbedsruns, everydata-cb-type="template-embed"element has already been replaced with the resolved template (wrapped in.pc-dynamic-render). The runtime sees nodata-cb-type="template-embed"in the DOM and silently skips the plugin, even with the registry entry pointing at the right URL. - Fix: when the
pc-dynamic-rendermarker is detected inenqueueRuntime, inject the plugin'sstyle.cssURL directly into the existingpagecraft_required_cssconfig array — the same pipeline that already emitscontentbox-runtime.cssat the{pagecraft_styles}placeholder via thetemplate_post_parsehook. That bypasses the lazy-load entirely; the browser gets the CSS via a normal<link>tag, the Bootstrap-row reset rules apply, and the rendered Dynamic Content section renders at full width with proper grid behavior.
Removed
- The diagnostic
ee()->logger->developer(...)line added in 0.9.25 has been removed. It served its purpose — the log told us the marker detection was working, which narrowed the bug down to the asset-fetch layer.
[0.9.25] — 2026-05-11
Diagnostic
- Added a one-shot
ee()->logger->developer()call insideenqueueRuntime()that logs the html length, whether thepc-dynamic-rendermarker is present, whether thedata-cb-type="template-embed"attribute is still present, and the current state of the static$registered_pluginsand local$new_pluginslists. Visible at CP → Tools → Logs → Developer Log after rendering any PageCraft-containing page. Lets us see exactly what enqueueRuntime is seeing per call — to figure out why template-embed registration is failing on staging despite the marker-detection code being in place. - Will be removed once we confirm the underlying behavior.
[0.9.24] — 2026-05-11
Fixed
.pc-dynamic-renderwidth now forced via!importantso it can't be shrunk by other rules. User confirmed that manually addingw-100(Bootstrap'swidth: 100% !important) fixed the rendered-page layout — the prior plainwidth: 100%was being overridden by something downstream (likely a framework rule with higher specificity, or flex-shrink reducing the basis). Also addedflex: 1 1 100% !importantso the wrapper definitely takes the full available width when sitting as a flex item inside.is-wrapper.- Canvas-side container neutralizer hardened with explicit selectors covering every class combination CB applies to the inner container (
is-container,is-container.v2,is-container.v2.is-content-1200,is-container.v2.is-content-1200.is-builder, plusis-container.v3and a wildcard[class*="is-content-"]). When two!importantdeclarations compete, specificity wins — and CB's own.is-content-1200 { max-width: 1200px }has specificity (0,1,0), so any of our 3- or 4-class selectors beats it cleanly. This forces the canvas wrapper chain to render at full width when wrapping a template-embed plugin, matching the rendered-page behavior.
Internal
- Canvas iframe cache buster bumped:
v=0.6.3→v=0.6.4.
[0.9.23] — 2026-05-11
Fixed
- Rendered Dynamic Content sections collapsed to mobile-mode layout on the front-end — content stacked vertically, section background only filled ~60% of viewport width, large empty space on the right. Root cause: ContentBox's
.is-wrapperelement isdisplay: flex; flex-flow: row wrap, so its direct children are flex items. CB's own.is-sectionrule forceswidth: 100%on those, but the new.pc-dynamic-renderwrapper added in 0.9.22 had no width rule — so as a bare flex item it collapsed to the minimum content width of the embedded template, which then rendered in mobile-mode layout (single-column cards, ~540px Bootstrap container width). Addingwidth: 100%; display: block; box-sizing: border-boxto.pc-dynamic-rendermakes it behave like a CB section and the rendered template gets the full viewport width to lay itself out against.
Internal
- Canvas iframe cache buster bumped:
v=0.6.2→v=0.6.3.
[0.9.22] — 2026-05-11
Fixed
- Rendered templates inside Dynamic Content sections were visually shifted to the right because ContentBox's runtime CSS contains an unscoped rule that clobbers Bootstrap's grid:
These match ANY.row { display: flex; gap: 2rem; margin: 0 -1rem } .column { padding-left: 1rem; padding-right: 1rem }.row/.columnon the page — including ones inside an embedded template's own Bootstrap layout. Bootstrap's.col-*widths sum to ~100% already, so CB's addedgap: 2rempushed content past the container's right edge, producing a visible sideways shift.
Changed
resolveTemplateEmbeds()wraps the rendered template in<div class="pc-dynamic-render">at output time. The wrapper is a CSS scoping anchor for the Bootstrap-row reset; without it, there's no reliable way to target "this is rendered template content, restore Bootstrap defaults here."mod.page_craft.php::enqueueRuntime()now enqueues the template-embed plugin's CSS on the front-end whenpc-dynamic-renderis present in the rendered HTML. Previously the plugin CSS only loaded in the canvas (via plugin auto-registration) and on entries that still carried thepc-section-template-embedwrapper marker — neither of which holds true on the front-end post-resolve. The new marker fallback closes that gap.assets/plugins/template-embed/style.cssre-emits Bootstrap 5's.rowgutter math scoped to.pc-dynamic-renderwith!important, beating CB's later-loaded clobber on equal specificity..col-*is unaffected by CB's.columnrule (different class name) so doesn't need a counterpart reset — but we re-emit the standard column padding for safety.
Internal
- Canvas iframe cache buster bumped:
v=0.6.1→v=0.6.2so existing canvas tabs pick up the new reset CSS without manual hard-refresh.
Note on the canvas-side container constraint
- Inside the canvas, the rendered template is still bounded by ContentBox's
.is-container.v2.is-content-1200wrapper chain (~1200px max-width). This is necessary for ContentBox's section/plugin tooling to function and is acceptable as a known visual difference: the canvas is "approximate," the front-end is "what the page actually looks like." A future release can revisit canvas display by suppressing the container max-width specifically inside.pc-section-template-embedsections.
[0.9.21] — 2026-05-11
Fixed
- Template selector now opens reliably on Dynamic Content sections — by bypassing ContentBox's click chain entirely for this UX. The 0.9.20 try/catch wrapper on
positionToolkilled the visible crash but didn't help: with the section-tool not yet injected, downstream code that would have triggeredopenContentEditoralso bailed silently, so the selector never appeared. Rather than chase every CB invariant our section happens to violate during early init, the template-embed plugin now attaches its own click handler to the placeholder and the rendered preview. Clicks open a self-contained modal (built in the canvas iframe, dispatched at z-index2147483647over everything) containing the template dropdown. On selection, the plugin writes the new path todata-pc-template-path/data-cb-template-path, fetches the chosen template via the existing preview endpoint, swaps the placeholder for the rendered HTML, and dispatches aninputevent so ContentBox's autosave picks up the change. - "Change template" pill added to the rendered preview's top-right corner (only visible in the canvas, marked
contenteditable=falseso it doesn't get saved into entry data). Clicking it re-opens the picker so editors can swap templates without trying to navigate CB's section toolbar. - The CB
editor.openContentEditorhook is still registered (kept for backwards compatibility with any CB code path that invokes it directly) but it's no longer the primary entry point for the picker.
Internal
- Canvas iframe cache buster bumped:
v=0.6.0→v=0.6.1so existing canvas tabs pick up the new picker without manual hard-refresh.
Why this works where 0.9.20 didn't
- 0.9.20's
positionToolguard prevented the visible error butD.select/D.clickContentstill depended on side effects ofpositionToolhaving succeeded (active section state, toolbar wiring) that we couldn't replicate from outside. The plugin'sopenContentEditoris invoked downstream of all of that, so it never ran. - 0.9.21 sidesteps the entire dependency by giving the plugin its own click → picker → render pipeline. No
.is-section-tool, nopositionTool, no CB selection state required. The plugin element itself is the surface; the click handler is on the plugin's own DOM.
[0.9.20] — 2026-05-11
Fixed
- Template selector never opened on Dynamic Content sections. Clicking the placeholder produced a flood of
Uncaught TypeError: Cannot read properties of null (reading 'querySelector') at Object.positionToolerrors andopenContentEditorwas never reached. Root cause traced by reading the minified ContentBox source:positionTool(t)doesl = s.querySelector('.is-section-tool')followed immediately byconst a = l.querySelector('.btn-move-section-up')— with no null guard between them. When a section's.is-section-toolhasn't been injected yet (which happens for template-embed sections during early canvas init and on first click after a fresh picker drop),lis null, and the secondquerySelectorcall explodes. The throw propagates up throughD.select → D.clickContent → D.builder.onPageContentClick, killing the click handler chain before it can invoke the plugin'sopenContentEditor. The section-tool is injected later bysectionSetup(), but for these sections it's not in place by the timepositionToolfirst fires. - Defensive wrapper around
builder.positionToolin the editor JS catches the throw, lets the click handler chain continue (soopenContentEditorruns and the template dropdown opens), and schedules a one-shot deferred retry 100ms later so the section toolbar still positions correctly oncesectionSetuphas had a chance to inject the tool. Localised patch — only the one CB method is wrapped, everything else is untouched.
Internal
- Canvas iframe cache buster bumped:
v=0.5.8→v=0.6.0so existing canvas tabs pick up the guard without manual hard-refresh.
Why this took several attempts to diagnose
- v0.9.9 and v0.9.17 changed the seed shape thinking the wrappers themselves were the problem. They weren't — the wrappers were necessary for CB tooling. The real bug was that even with the correct wrapper chain, CB's
positionToolhas an unguarded null dereference that fires whenever a section's tool element isn't ready yet, and template-embed sections happen to hit that timing window. Once the diagnosis traced into the actual CB source (via the minified file's error column number), the fix was a 15-line wrapper.
[0.9.19] — 2026-05-10
Front-end-only fix for Dynamic Content rendering distortion. The canvas is left fully intact — the wrapper chain (is-section → is-overlay → is-container → row → column → embed-div) stays around the embed in the editor so ContentBox's section/plugin tooling keeps working. At output time, the entire pc-section-template-embed wrapper is replaced with just the rendered template, so the front-end gets clean output with no CB-wrapper interference.
Changed
mod.page_craft.php::resolveTemplateEmbeds()now replaces the surroundingpc-section-template-embedsection (when found) instead of just the embed div. Walks backward from eachdata-cb-type="template-embed"occurrence (capped to 4KB to avoid spurious matches) for the nearestpc-section-template-embedmarker, verifies it's actually a class-attribute match, finds the section's matching</div>via depth-counted forward scan, and splices the rendered template in as a complete replacement for the wrapper span. Falls back to embed-only replacement when no wrapper is found within range (legacy entries from before the marker class existed, hand-edited content). Sanity check: the embed's</div>must close before the section's</div>closes for the wrapper to count as enclosing — guards against the marker being on a different (already-resolved or sibling) section.- Pure string scanning, no DOMDocument anywhere. New
findMatchingDivClose()helper does the depth-counted forward scan that the previous code inlined; both the embed-close and section-close lookups use it. Splicing issubstr()only — the rendered template's HTML is never re-serialized through a parser, so the HTML5 mangling that 0.9.12 exposed (script/style content corruption, self-closing tag rewrites, SVG damage) cannot happen.
Why this is different from 0.9.12 and 0.9.17
- 0.9.12 attempted whole-section replacement via DOMDocument round-trip — mangled HTML5, broke pages site-wide.
- 0.9.17 attempted to slim the saved canvas shape by collapsing
.is-sectionanddata-cb-typeonto one element — broke ContentBox's section-handling code. - 0.9.19 leaves the canvas/saved shape exactly as v0.9.16/v0.9.18 had it (full wrapper chain, plugin tooling intact). Only the front-end output changes, via a string-only splice that targets the section boundaries we already control.
What you can verify
- Canvas: unchanged. Drop a Dynamic Content section, pick a template, edit the entry, save — works exactly as in v0.9.18. The canvas still shows the wrapper chain because that's what ContentBox needs to host editor tooling.
- Front-end: view the published page or
/index.php/your-entry/— the rendered template should now occupy its natural width and layout, with nois-container.v2.is-content-1200cap, nois-builder/.row/.columninterference. View source: where you used to see the full wrapper chain wrapping the rendered template, you should now see the rendered template at the same DOM level as its sibling sections.
[0.9.18] — 2026-05-10
Revert release. Rolls back 0.9.17's single-element Dynamic Content seed, which broke ContentBox's section-handling code.
Reverted
- 0.9.17 — single-element seed and migration. Putting
class="is-section ..."anddata-cb-type="template-embed"on the same DOM element confused ContentBox: its section-handling walks up from a plugin element looking for an enclosing.is-sectionancestor, found itself, then crashed incloneNode(during save serialization) andquerySelector(duringpositionToolwhen an editor clicked into the canvas). Net result: clicking a new Dynamic Content section produced a flood ofCannot read properties of null (reading 'querySelector')errors and the template selector never opened. Same shape problem as 0.9.9, just expressed differently —.is-sectionand the plugin element have to be distinct DOM nodes for CB to function. - The render-time container-class injection (
Services/TemplatePreview::injectContainerClass()) and canvas warning banner from 0.9.17 are also reverted. They were neutral on their own, but they only made sense paired with the slim seed — with the wrapper-chain shape back in place, injectingis-container v3onto a template element nested inside CB's existingis-container v2.is-content-1200doesn't do anything useful (CB's outer container still applies its width cap).
Behavior restored
- Dynamic Content seed is back to the 0.9.16 shape: full
is-section → is-overlay → is-container.v2.is-content-1200.is-builder → row → column → embed-divchain. Plugin selector works, save/load works, canvas tooling works. Template still gets the wrapper-induced layout distortion when its own grid framework collides with CB's.row— that's the unsolved problem 0.9.17 tried to fix and we'll re-attempt with a different shape later.
Migrated
- The 0.9.17 migration corrupted the seeded
Dynamic Contentrow inpagecraft_sectionsto the broken single-element form. The DB row was restored manually for this install. Future installs and updates use the original v0.9.16 wrapper-chain seed.
What we learned (worth not re-litigating)
class="is-section"+data-cb-type="..."on the same element is a no-go. CB's internal model assumes plugin elements are children of sections, not the section itself.- Any further attempt at slimming the Dynamic Content shape needs to keep
.is-sectionanddata-cb-typeon separate nodes. The minimal viable structure is at least<div class="is-section pc-section-template-embed"> <div data-cb-type="template-embed">...</div> </div>— but as 0.9.9 showed, that combo (without the row/column chain) also confuses CB's plugin scanner. We need to find what CB actually requires, not what we hope it tolerates.
[0.9.16] — 2026-05-09
Fixed
- Image uploads from the canvas were 403'ing with a non-JSON HTML error response (
Uncaught SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON). Root cause: the canvas iframe captures EE's session-tied CSRF token at iframe-load time and caches it in a JS variable for the life of the tab. When EE's CSRF token later rotates (login/logout, session refresh, some middleware events), the cached canvas token goes stale. EE'sprocess_secure_forms()(legacy/libraries/Core.php line 942) then rejects the upload POST with a 403 + HTML "csrf_token_expired" page. Because the canvas'suploadhandler doesawait res.json()unconditionally, the HTML body explodes the JSON parser and ContentBox's image-insert flow silently fails. This is the same class of failure that affects any long-lived canvas tab, not something specific to a particular session.
Changed
Page_craft::uploadACT is nowcsrf_exempt = 1. Defense-in-depth replacement:Actions/Upload.phpnow does a same-origin check (Origin or Referer header must point at this site'ssite_urlhost) in lieu of CSRF token validation. Browsers always send Origin on cross-origin POSTs and never let attacker JS forge it, so a forged form onattacker.comis detectable even with cookies attached. The auth check (member must be logged in) and upload-destination permission check (member must have access to the destination) are unchanged. Net security posture: equivalent for legitimate users, no longer breaks on session token rotation.- New
Page_craft_upd::markUploadCsrfExempt()runs on both install() and update(). Setscsrf_exempt = 1on the upload action row inexp_actions. Idempotent.
Migrated
- Existing installs running update() will get the upload action flipped to
csrf_exempt = 1automatically. No manual DB step required.
[0.9.15] — 2026-05-09
First step of the Grid Builder UX overhaul (designs in Figma — six frames reviewed). This release fixes the two long-standing bugs in the column-split flow without adding any new UI; subsequent releases will land the per-column toolbar and the split-aware block picker.
Fixed
- New column wasn't clickable until you saved and reloaded after a horizontal split. ContentBox's selection runs on
mousedown(notclick), and only fires for elements that havedata-click="true". The freshly-created<div class="column">was inserted without that attribute, and the post-insertnewCol.click()was firing beforeapplyBehaviorOnfinished re-attaching CB's handlers — so the click event reached an element CB hadn't yet decorated, and selection silently failed. New activation helper seedsdata-clickon the new column up front, clearscell-activefrom siblings to reset visible selection, and dispatches a real bubbling mousedown/mouseup/click sequence that CB's delegated handlers actually listen for. Save round-trip is no longer required. - Parent column became unselectable after a vertical split.
splitVertical()was strippingdata-click="true"along withcontenteditablefrom the outer column on the assumption that "ContentBox will make the inner columns editable" — but that also removed the parent's CB selection hook entirely, leaving no way to click back to the outer wrapper to apply column-level settings or undo the split. The fix keepsdata-click="true"on the parent (onlycontenteditableis removed, since the outer is now structural) and setsdata-click="true"explicitly on both new inner columns so CB recognises all three as click targets without waiting forapplyBehaviorOnto redecorate them. Clicking inside a sub-cell selects the sub-cell; clicking the parent's gutter/border area selects the parent. Added apc-gb-vsplit-parentmarker class to the parent for future styling hooks.
Internal
- Canvas iframe cache buster bumped:
v=0.5.5→v=0.5.8so existing canvas tabs pick up the new split logic without manual hard-refresh.
Coming next
- Per-column right-edge
+ ⋯ 🗑mini toolbar (matches the Figma design's visual paradigm — same trio that lives on the section toolbar, just oriented vertically and column-scoped). - Block picker with Add to Left / Add to Right tabs when invoked inside a split column.
- Section/column border colour scheme to match the design (peach for sections, dashed blue for active columns).
[0.9.8] — 2026-05-07
Fixed
- Documentation page TOC visually merged with the PageCraft tabs sidebar. The
pc-docs-layoutgrid had no padding so the TOC column sat flush against the white panel's left edge, leaving no visual gap between the addon's tabs sidebar and the docs TOC. Addedpc-page-body-equivalent padding (24px / 32px), a vertical divider on the right of the TOC, and tightened the TOC width from 240px → 220px. - Group banner missing in body. The User Guide / Developer Reference labels appeared in the TOC but the body just concatenated all sections with no break, so a reader scrolling through couldn't see when they crossed from User Guide into Developer Reference. Body now renders a pill-style "User Guide" / "Developer Reference" banner immediately before each group's first section.
- Markdown body typography tuned for tighter heading hierarchy (h1 24px / h2 17px / h3 14px) and slightly more generous line-height for paragraphs and lists.
- Narrow-viewport breakpoint widened from 800px → 900px so most laptop windows still get the two-column layout.
[0.9.7] — 2026-05-07
Added
- Comprehensive in-CP documentation. The CP → Add-Ons → PageCraft → Documentation page now renders an eleven-section guide split into a User Guide (Installation, Getting Started, Editing Sections, Nav Builder, Global Header & Footer, Grid Builder, Dynamic Content) and a Developer Reference (Editing Existing Templates, Importing Custom Templates, The Class Marker System, Template Tags Reference). A sticky left-rail TOC anchor-links every section. ~5,900 words total.
- Source of truth in
docs/. All documentation is authored as Markdown underdocs/at the addon root and rendered into CP at request time, so GitHub readers and content editors see the same content with no manual sync. Files:00-installation.mdthrough13-template-tags-reference.md, plusdocs/README.mdas the GitHub-facing index. Services/MarkdownRenderer.php— a 170-line in-house Markdown → safe HTML converter sized for the addon's needs (headings with slug ids, paragraphs, single-level lists, fenced and inline code, bold/italic, links). Rejectsjavascript:/data:URL schemes; allows http/https/mailto and relative paths.- Linked the existing
themes/user/pagecraft/developer-reference.htmlfrom the new documentation page so site builders can jump to the annotated HTML examples.
Changed
- Root
README.mdnow points atdocs/for the full guide instead of carrying inline tag references. Installation instructions condensed since ContentBox is bundled (the README previously asked users to install ContentBox separately, contradicting v0.9.1's vendor bundling).
[0.9.6] — 2026-05-06
Fixed
- Bloqs fieldtype parser annotations leaked into the editor canvas preview as visible text (
{!-- bloqs:start:95:0 --}/{!-- bloqs:end:95:0 --}). The existing strip inTemplatePreviewonly matched annotations whose tail was a 16+ char hex run (ra:HEX); Bloqs uses a numericstart:N:Nshape so it slipped through. Generalised the regex to match{!-- word(:token)+ --}where each token is alphanumeric/underscore/hyphen and contains no spaces — coversra:,bloqs:start:N:N,bloqs:end:N:N, and any future addon-style annotation, while still leaving legitimate human comments like{!-- TODO: fix this --}alone (those have spaces).
[0.9.5] — 2026-05-06
Fixed
template_listACT was never registered inexp_actionson existing installs, causinggetActionId('template_list')to return 0, which produced URLs likeindex.php?ACT=0(a 200-OK empty body) when the template-embed plugin tried to populate its dropdown. Two-part fix:- Added
template_listandtemplate_ingesttoPage_craft_upd::$actions(they were declared inaddon.setup.phpbut missing from the upd class, so EE never auto-inserted them on update flows). - New
ensureAllActionsRegistered()runs on every update: iterates$this->actionsand inserts any row missing fromexp_actions. Self-heals against any future ACT we forget to migrate. Idempotent.
- Added
[0.9.4] — 2026-05-06
Fixed
- Section picker silently lost Grid Builder + PageCraft (and every other PageCraft pack registered after a missing pack). The default
template_packssetting included the three Crafto packs (crafto-corporate,crafto-elements,crafto-premium), which we deliberately don't redistribute. On any install that didn't already own those packs, the canvas tried to loadcrafto-corporate/templates.js→ 404 → ContentBox's templateList processor halted on the JS error and never reached the entry that loadspagecraft-sections.js. Result: the canvas picker showed only Simple Start / Quick Start / Animated and skipped everything after the first missing pack — even though the data was correctly present in the DB and in the JS file. Two-part fix:- Default
template_packsno longer enables the Crafto packs. Users who own them can enable in CP → Add-Ons → PageCraft → Block Library. EditorFrame::process()now filters the templateList to packs whosetemplates.jsactually exists on disk (logs the skip to developer log). Self-heals against any future missing-file scenario.
- Default
- Canvas iframe cache bump
v=0.5.4 → v=0.5.5so existing entry-edit tabs pick up the new server-side template list on reload.
[0.9.3] — 2026-05-06
Added
- "Regenerate File" button on CP → Add-ons → PageCraft → Sections. Manually rewrites
pagecraft-sections.jsfrom the current DB contents and reports the outcome inline. Existed silently inPage_craft_upd::regenerateSectionsFile()before but failures were swallowed and there was no way to retry. Now surfaces the exact reason (e.g. "file is not writable by the PHP user — fix permissions on …") so remote-host installs can self-diagnose.
Fixed
SectionFileWriterno longer silently overwrites a non-empty bundled JS file with empty arrays when the DB has no categories/sections yet. Previously, running install/update on a fresh DB clobbered the bundledpagecraft-sections.js(which carries Grid Builder + PageCraft data out-of-box), leaving the canvas picker empty. The writer now refuses the destructive write, logs to the developer log, and returns success so the bundled file stays intact until real data exists.SectionFileWriterwrite failures now throw aRuntimeExceptionwith a permission-aware hint instead of silently returningfalse. The new "Regenerate File" CP button catches the exception and shows the real cause; install/update logs it and continues so the rest of the migration completes. Root cause of the remote-server "Grid Builder and Dynamic Content sections show in the manager but never on the canvas picker" report.
[0.9.2] — 2026-05-06
Added
- Seed default Grid Builder + PageCraft sections on install/update. The bundled
pagecraft-sections.jslisted Grid Builder columns (1–5 col, narrow/wide pairs) and the PageCraft "Dynamic Content" template-embed section, but neither the categories nor the sections existed inexp_pagecraft_section_categories/exp_pagecraft_sectionson a fresh install — so whenSectionFileWriterran it overwrote the bundled JS with empty arrays and the picker lost both categories. NewPage_craft_upd::seedDefaultSections()inserts them on every install and on every update; idempotent (matches existing rows by name and skips them, never touches user data).
Fixed
template-embedplugin's "Failed to load templates" surfaces the real error and is no longer selectable. When the template-list fetch returned an error or non-array shape, the plugin appendedFailed to load templatesas a regular<option>whose value defaulted to the text — selecting it (or any change event firing on it) saved that string as the entry's template path. Now the option isdisabled, the actual error message is appended (Failed to load templates: <reason>), and the underlying failure is logged toconsole.errorso remote-install issues are diagnosable from DevTools. Empty results render a separate disabled "No templates found on this site" option. Also added explicitcredentials: 'same-origin'to the fetch.
[0.9.1] — 2026-05-05
Added
- ContentBox.js vendor files now bundled in the repo. The full
themes/user/pagecraft/tree — ContentBox 5.x core, ContentBuilder, the front-end runtime, every stock asset directory (fonts, styles, scripts, modules, etc.), and the stock block / template packs (minimalist-blocks,templates-quick,templates-simple,templates-animated,coporate) — ships with the addon. Fresh installs are now self-contained: no separate ContentBox download required. Vendor files are redistributed under the Bison Digital developer license; downstream users redistributing must purchase their own. - Auto-regenerate
pagecraft-sections.json install/update. NewPage_craft_upd::regenerateSectionsFile()runsSectionFileWriter::write()after install and after every update, rewriting the section picker's JS data with the local install's actual theme URL. Fixes the case where a fresh download carried the snapshot URLs from another host.
Fixed
- Hardcoded
default_site/template directory broke template lookups on EE installs with a non-defaultsite_short_name. Three call sites (mod.page_craft.php::template_list,ft.page_craft.phptemplate-embed render,Services/TemplatePreview.php::loadTemplate) now readee()->config->item('site_short_name')and fall back todefault_siteonly when unset. This was the root cause of "Dynamic Content template selector fails to load templates" on remote installs. pagecraft-sections.jsshipped with absolutehttps://ee-pb.ddev.site/...thumbnail URLs, breaking the section picker on every install but the original dev box. Replaced with root-relative/themes/user/pagecraft/...paths so the file works out-of-box on any host before the writer ever regenerates it..gitignorecleaned up. Removed the ContentBox vendor exclusion section (since vendor files ship now). Added explicit ignores forthemes/user/pagecraft/imported/,assets/Archive.zip, and the third-party Crafto packs that aren't redistributable.- Theme README rewritten — single-step install instructions; no second-stage ContentBox manual install needed.
[0.9.0] — 2026-05-04
Added
- PageCraft theme assets ship in the addon repo. Previously the addon's
themes/user/pagecraft/shipped with only a README, leaving fresh GitHub installs without the custom plugins, block packs, and section templates the addon depends on. The repo now tracksassets/plugins/,assets/pagecraft-custom/,assets/monochrome-blocks/,assets/grid-builder/, andpagecraft-sections/. ContentBox vendor folders (fonts/,styles/,modules/, etc.) remain ignored and must still be installed separately per the theme README. - Per-item visual modal on
.nav-link(nav-menu plugin). Clicking a menu item in the canvas now shows a floating toolbar (Edit / Delete) and an Edit modal with the full visual field set: BG/Text colors, font size/weight, width, border style/thickness/radius, padding (L/R + T/B), and 4-side margins. Mirrors the existing.nav-btnmodal pattern. Submenu links (.submenu-link) share the same handler. Toolbar omits Delete whendata-cb-use-structure-nav="true"(Structure-mode menu set is read-only). - Per-item toolbar trimmed to 3 icons. CTA buttons show Edit / Link / Delete; menu items show Edit / Delete (or Edit only in Structure mode). The previous Duplicate icon on
.nav-btnwas removed for consistency.
Fixed
- Toolbar capture-guard no longer kills its own click handlers. The nav-menu plugin's editor-mode capture-phase guard at
index.js:1819previously called bothpreventDefault()ANDstopImmediatePropagation()on any<a>click inside the nav. The latter terminated the event before it could reach the.nav-btn/.nav-linkbubble-phase handlers, so the Edit/Link/Delete toolbar never appeared. RemovedstopImmediatePropagation();preventDefault()alone still blocks link navigation in editor mode. addon.setup.phpversion bump 0.8.0 → 0.9.0 so EE's update flow recognises that staging installs upgraded from older snapshots have a newer version available.CHANGELOG.mdbackfilled with previously undocumented 0.6.0 / 0.7.0 / 0.8.0 entries (see below) so the file no longer lags the codebase.
Internal
EditorFrame.phpcanvas cache version bumpedv=0.5.3 → v=0.5.4so iframes re-fetch with the toolbar fix.
[0.8.0] — 2026-05-04
Added
- Template Embed canvas preview. The editor canvas now renders the actual EE template HTML inside
data-cb-type="template-embed"blocks instead of a static placeholder. NewServices/TemplatePreview.phpextracts template lookup + parse so the editor preview ACT and the front-end render share one code path. Strips{!-- ra:HEX --}parser-internal markers; lazy-loadsee()->TMPLvia the EE library loader (TMPL is not auto-present in ACT context); zeroessession->member_idduring parse so{logged_in}/{logged_out}resolve to the public branch. Actions/TemplateRenderPreview.php— JSON ACT, output-buffered so stray PHP notices surface in{error}instead of corrupting the JSON envelope.- "PageCraft" section category + "Dynamic Content" section seeded for the picker so editors can drop a template-embed via Sections (not Blocks).
Changed
mod.page_craft.php::resolveTemplateEmbeds()refactored to callTemplatePreviewservice.- Both
mod.page_craft.phpandft.page_craft.phpresolvers now preferdata-pc-template-pathand fall back todata-cb-template-pathfor back-compat. Thedata-pc-*namespace survives ContentBox's openContentEditor clone-diff propagation; thedata-cb-*namespace was being silently stripped. Actions/EditorFrame.phpinjectswindow._pcTemplateRenderUrlinto the canvas iframe (mirrors the existing_pcTemplateListUrlpattern).
[0.7.0] — 2026-05-01
Added
- Structure-driven Navigation toggle on the nav-menu plugin. When enabled (
data-cb-use-structure-nav="true"), the rendered nav auto-generates from the EE Structure page tree (top level + 1 sublevel), respects each page's "Hidden in nav" flag, and orders by Structure's nested-setlft. Services/StructureNav.php— single source of truth for the nav data shape (entry_id,label,href,hasSubmenu,children).Actions/StructureNavPreview.php— JSON ACT consumed by the editor canvas for preview parity with the published render.mod.page_craft.php::global_header()/global_footer()call newmaybeInjectStructureNav()to swap nav-list contents at render time. Uses string scanning (not DOMDocument) to avoid mangling ContentBox HTML5 fragments. Addsaria-current="page"matchingee()->uri->uri_string().Actions/EditorFrame.phpinjectswindow._pcStructureNavUrlinto the canvas iframe.- Preview button on the publish-form toolbar — opens the entry's Structure URI in a new tab (resolves via
site_pageswith{base_url}/substitution).
Fixed
- Custom JS / Custom CSS robustness.
Actions/CustomJs.phpandActions/CustomCss.phpnow strip every<script>/<style>/<!-- -->tag (not just outer wrappers) so editors who paste compilations like<script>…</script><script>…</script>don't break ContentBox init. Cache headers tightened tono-store+no-cache+Pragmaso a stale broken response can't survive a fix. Actions/EditorFrame.php— custom_js is no longer injected into the canvas iframe (it requires front-end libs the canvas doesn't load — typicalReferenceErroron jQuery / Revolution slider halted ContentBox init).Actions/SaveGlobalComponent.phpstripsdata-cb-original-content(ContentBox's per-mount snapshot, otherwise restored on every load and clobbers fresh content) anddata-pc-manual-html(legacy backup attr that compounded encoding through repeated round-trips).ft.page_craft.php— when a field's saved HTML is empty, seed the canvas with a minimal.is-section+.is-overlay+.is-container.v2.is-content-1200.is-buildershell so ContentBox has a drop zone. Without this, drag-from-picker silently failed because there was nothing to drop into.
[0.6.0] — 2026-04-15
Added
- TemplateIngest — imports Crafto HTML template packs into the sections DB, rewrites asset URLs, auto-generates dark-background SVG thumbnails, and adds
is-subblock editmarkers for ContentBox inline editing. - PackImport — bulk JSON section/category import (replaces or skips existing).
- Grid Builder category — preset 1–5 column ContentBox row/column layout starters.
- Horizontal / Vertical split buttons in the ContentBox column "…" popup; vertical split divides the active column into stacked nested halves.
- Save as Section strips ContentBox editor UI artefacts (
is-tool,is-rowadd-tool,is-box-tool,contenteditable,data-click,draggable,box-select) before saving. SectionFileWriter— absolute thumbnail URLs supported via empty path config;is-subblock editinjected forpc-edit/pc-imgmarkers on JS regeneration.- Developer reference template added to
themes/user/pagecraft/.
Fixed
- Template picker thumbnails now render correctly (
path:''avoids double-URL). - Imported sections are editable immediately (
is-subblock editon allh*/p/img). - Split buttons now read
builder.activeCol/.cell-activecorrectly; removed brokenclosest('.column')mousedown approach.
[0.5.0] — 2026-04-11
Added
- Global Footer — lock/unlock a footer section as a site-wide component, mirroring the Global Header feature. Super admins see padlock icon buttons injected into the section toolbar (
.is-section-tool) when hovering any section taggeddata-pc-type="footer". Locking saves the section HTML + CSS toexp_pagecraft_global_components(component_key = footer) and renders it on all pages via{exp:page_craft:global_footer}. {exp:page_craft:global_footer}template tag — renders the locked global footer. Accepts the same parameters asglobal_header:wrapper,class,runtime. Returns empty string when no locked footer exists, making it safe to add to all templates unconditionally.- Quick Start footer templates tagged — all 9 footer section templates in
templates-quick/templates.jscarrydata-pc-type="footer"on their root.is-sectiondiv so the toolbar buttons appear automatically after inserting a footer from the template picker.
Changed
- Global Header lock/unlock moved to section toolbar — the Lock/Unlock buttons previously injected into the ContentBox
.submainsidebar panel (triggered by selecting the Navigation Menu block) are now injected into.is-section-toolinstead, matching the footer pattern. The buttons appear on hover of the nav-menu section, no sidebar interaction required. - Section toolbar cleanup — both header and footer save paths now strip
.is-tooland.is-rowadd-toolrow-level editor elements in addition to the existing section/column/element tool classes, preventing ContentBox editor controls from appearing in saved global component HTML. onChangefeedback loop resolved — the periodic autosavesetIntervalpreviously calledbuilder.html()for change comparison, which caused ContentBox to fireonChangeinternally, creating an infinite save loop. The interval now callsdebouncedSave()directly. An_isSavingguard also suppresses re-entrantonChangecallbacks during serialization.
Fixed
- SVG
classNameisSVGAnimatedString, not a plain string — calling.replace()on it threw a silentTypeErrorthat aborted the lock save handler. All class cleanup in save paths now usesgetAttribute('class')/setAttribute('class', …)instead. SaveGlobalComponent.phpskeleton check (previously required 2000 chars +data-cb-typeblocks) rejected all footer saves because footer sections are plain HTML without ContentBox data blocks. Relaxed to a simple 100-character minimum.
[0.3.0] — 2026-03-23
Added
- Per-field content library control — field settings now expose "Allowed Block
Packs" and "Allowed Block Categories" checkbox groups. Leaving both empty
inherits the global addon settings; selecting specific items restricts that
field to only the chosen packs and categories. Stored as JSON in
field_settings; empty string = inherit. field_idthreading — the editor frame URL now includes?field_id=N; bothEditorFrameandBlocksJsread that parameter and resolve per-field pack/ category settings before falling back to global settings.- Expanded
pack.jsonmanifest schema — packs can now declaredescription,author, andauthor_url. All three are optional and displayed in the settings page pack cards and the per-field allowed-packs checkbox labels. - Settings sidebar nav — CP settings split into four sub-pages (Global Settings, Editor Permissions, Advanced Settings, Documentation) accessed via a persistent left-hand sidebar nav, matching EE's native sidebar pattern.
- Color palette table — custom color palette upgraded from plain textarea to a structured swatch + hex + name table with drag-to-reorder rows and trash-icon delete buttons.
lock_colorssetting — new boolean toggle restricts editors to the custom palette only, hiding the free-pick color input in the editor.- Documentation sub-page — built-in reference for template tags and field tag usage, accessible from the sidebar nav.
Changed
- Settings page reorganized — sections now group by domain: General → Content Library (packs + categories combined) → Section Templates → Appearance (color palette, custom CSS/JS) → Permissions → Advanced (AI, canvas assets).
- Settings page CSS consolidated — all custom styles moved into a single
pageStyles()block injected viaee()->cp->add_to_head(). No more per-render inline<style>blocks or duplicated rules. - Native EE field types — Custom CSS, Custom JS, and Color Palette fields now
use
type: 'textarea'instead of raw HTML. CodeMirror and color swatch preview are applied progressively via a singlepageScripts()foot block. - Canvas Assets simplified — inputs now accept plain URLs instead of raw
<link>/<script>tags. The server generates the correct tag on save. Existing stored tags are migrated transparently on load via URL extraction. - Canvas Assets split — separate CSS and JS asset lists replace the single
mixed
canvas_assetslist. Existing entries are migrated by type on first load. - EE native classes — permissions table uses
mainTable; canvas asset "Add" buttons usebutton--default button--small. Custom CSS replaces all hardcoded#4d78d1with--pc-primaryCSS custom property. - Settings slideout removed — the cog button in the editor toolbar now links
directly to the addon settings page (
target="_blank") instead of opening an iframe slideout panel. Removes iframe coupling and aligns with EE CP conventions. - AI settings moved to Advanced — AI toggle and OpenRouter API key moved from the General section to a new Advanced section alongside Canvas Assets.
- Card grids unified — Block Categories and Section Template Packs grids now
use
pc-grid-wide(200 px minimum column width), matching the Installed Block Packs grid. - Equal-height card footers — cards use a flex column layout so the gray footer background always extends to the bottom of the card regardless of content length, keeping all cards in a row visually aligned.
- Pack card metadata — settings page pack cards now show description, linked author name, version, and category count on a single meta line.
[1.3.0] — 2026-03-17
Added
- Color palette — define brand hex colors in CP settings; passed as the
colorsarray to ContentBox so they appear as swatches in the color picker. One hex value per line; invalid values are silently ignored. - Canvas Assets manager — replace the single
editor_stylesheet_urlfield with separate CSS and JS asset lists. Each entry is a raw<link>or<script>tag injected into the editor canvas iframe. Also exposed on the front end via{pagecraft_css}and{pagecraft_js}template tags. - Settings slideout — a cog icon in the editor toolbar opens the addon settings page in a slide-in panel without leaving the publish form. (Replaced by a direct settings link in the next release.)
{pagecraft_css}and{pagecraft_js}template tags — output the Canvas Assets CSS/JS tags on the front end so canvas-specific files can be loaded for site visitors as well as editors.
Changed
editor_stylesheet_urlsetting superseded bycanvas_css_assetsandcanvas_js_assets. Existing single-URL values are read as a legacy fallback and migrated on first save.upd.page_craft.phpupdate()seedscanvas_css_assets,canvas_js_assets, andcolor_palettesettings rows for installs upgrading from 0.2.0.
[0.2.0] — 2026-03-17
Added
- Add-on block pack support — drop any block pack into
themes/user/pagecraft/assets/{pack-name}/with acontent.jsand apack.jsonmanifest and it is auto-discovered by PageCraft. Enable or disable it globally from CP > Add-Ons > PageCraft. pack.jsonmanifest format — declares pack name, version, contentJs filename, and a category map (id → {label, preview}). Built-in labels take priority for overlapping IDs; pack-only IDs are appended automatically.blocks_jsEE action (Actions/BlocksJs.php) — dynamically merges all active packs into a single JavaScript response served as ContentBox'ssnippetUrl. Uses IIFE scoping to preventvar data_basiccollisions across packs. Rewrites all image and thumbnail paths to absolute URLs server-side so ContentBox requires nosnippetPathorsnippetPathReplaceconfiguration.snippetCategoriespassed to ContentBox — controls which category tabs appear in the block picker and their display labels.- Installed Block Packs settings section — visual card grid showing every auto-discovered add-on pack with an enable/disable toggle, version, and category count.
- Global block category management — enable or disable any built-in or pack-only block category via a visual thumbnail grid in CP settings. Pack-only categories are shown with a source badge.
- Global section template pack management — enable or disable Simple Start, Quick Start, and Animated template packs individually.
- Role-based editor permissions — map each EE member role to a permission
level. Full gives access to all ContentBox features. Content hides
per-element settings panels (
elementTool: false) while preserving text editing, file uploads, and block insertion. Configured per-role in CP settings and enforced at editor load time. - Custom Editor Stylesheet URL — a global setting to inject any web-accessible CSS file into the ContentBox canvas iframe, so theme utility classes (e.g. a CSS framework) render correctly while editing.
Fixed
- Category IDs from
pack.jsonare JSON-decoded as PHP integers; these were passed as integers into the JavaScript filter array, causingArray.indexOf(String(category))to silently drop all pack-only snippets. All category IDs are now normalised to strings beforejson_encodeinBlocksJs,EditorFrame, and the MCP save handler. - Setting
basePathin the ContentBox constructor caused ContentBox to unconditionally overwrite the customsnippetUrlwith a hardcoded path to the built-incontent-plugins.js, so add-on packs were never loaded.basePathis removed; all paths are now passed explicitly. - UTF-8 BOM (
EF BB BF) at the start of add-on pack JS files caused a silent parse error when the content was placed inside an IIFE body. BOM bytes are now stripped before wrapping. - Path absolutization order in
absolutizePack— general path replacement now runs before thumbnail replacement to prevent absolute URLs injected in step 4 from being double-replaced in step 3. _snippets_pathin pack JS files is now set to an empty string rather than the absolute URL; ContentBox reads this variable as its effectivesnippetPathand was prepending it to already-absolute thumbnail paths, producing triple URLs.
Changed
upd.page_craft.phpupdate()method registers theblocks_jsaction and seeds new settings rows (block_categories,enabled_packs,template_packs,editor_permissions,editor_stylesheet_url) for existing installs upgrading from 0.1.0.
[0.1.0] — 2026-03-16
Initial beta release.
Added
- PageCraft fieldtype (
page_craft) — embeds a ContentBox.js visual page builder inside an ExpressionEngine publish-form iframe. - Upload action — proxies file uploads through EE's native upload library
so files are registered in
exp_filesand appear in the File Manager. - File picker bridge — opens EE's native file browser inside a ContentBox filePicker modal, allowing existing files to be inserted without re-uploading.
- Editor frame action — self-contained ContentBox editor page loaded inside
an
<iframe>to keep ContentBox UI scoped away from EE's CP chrome. - AI proxy actions (
ai_proxy,ai_proxy_stream) — proxies ContentBox AI Code Chat requests to OpenRouter, supporting streaming (SSE) and non-streaming modes. Configurable model lists in addon settings. template_post_parseextension — replaces{pagecraft_runtime}placeholder in EE templates with the ContentBox runtime<script>block, enabling plugin blocks on the front end.- Front-end template tags:
{field_name},{field_name type="html"},{field_name type="css"},{field_name type="maincss"},{field_name type="sectioncss"}. - Module template tags:
{exp:page_craft:render},{exp:page_craft:html},{exp:page_craft:css},{exp:page_craft:runtime_assets}. - Addon settings page — global upload directory, editor height, AI toggle, and OpenRouter API key (stored encrypted).
- Per-field overrides — upload directory and editor height can be overridden at the individual field level.
- Device preview toolbar — fullview (100 % canvas) default; device buttons constrain the canvas to desktop-lg, tablet-landscape, tablet, and mobile widths; expand button returns to fullview.
- All 32 ContentBox plugin blocks registered via
ContentBoxRuntimein both the editor canvas and on the front end. - CSRF-safe uploads:
X-Requested-With: XMLHttpRequestandX-CSRF-TOKENheaders sent with every upload request to satisfy EE's AJAX-exempt path.
Requirements
- ExpressionEngine 7.x
- PHP 8.1+
- ContentBox 5.x (installed separately into
themes/user/pagecraft/)