| builder | ||
| lib | ||
| public | ||
| test | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| index.js | ||
| package.json | ||
| README.md | ||
theme-builder
A Saltcorn plugin: a visual theme builder. By default it ships a Bootstrap-5 CSS
custom-property skin overlay (no Sass, not a layout) that composes with the
active theme (e.g. sbadmin2). Turning on layout mode (the layoutMode
setting) flips the plugin into a selectable layout whose wrap() is
assembled from a layout tree, with vendored Bootstrap and an optional dart-sass
recompile.
The authoritative design is in ARCHITECTURE.md. This
README covers what is built, how it is stored, security, and how to run it.
Status (layout mode built + validated)
Layout mode is built and validated live. The plugin registers as a real
Saltcorn layout when layoutMode is on, and its wrap()/authWrap() render the
page from the active theme's layoutTree (layoutTree.js presets
topnav/sidebar + renderTree.js server-side emitter), linking vendored stock
Bootstrap (public/themeBootstrap.*) + the theme overlay. The active tree is
persisted in plugin config on activate (so wrap() stays synchronous and it
rides backup). The overlay header is suppressed per-request for
theme-builder-served requests; configWorkflow hard-blocks enabling layout mode
until layout_by_role covers every live role. The editor gains a Layout
panel (preset picker) when layout mode is on. Sass remains the documented
opt-in (default engine is the overlay on vendored Bootstrap); the full Craft.js
drag-drop canvas (ARCHITECTURE 8.7) is the remaining authoring enhancement.
Layout mode validated (in-process harness + real HTTP, all green): enable ->
registered in getState().layouts; wrap() renders tb-root + navbar/sidebar +
body + theme CSS; authWrap() renders the login form (real HTTP login works);
GET / served by our layout; overlay suppressed exactly for theme-builder
requests.
Deep overlay (buttons recolor without Sass)
A plain :root{--bs-primary} override does not recolor .btn-primary —
Bootstrap 5.3 bakes button/component colors at Sass compile time. The deep
overlay (lib/bootstrapColor.js + lib/deepOverlay.js) faithfully ports
Bootstrap's shade/tint/color-contrast math to JS and re-emits the per-
component --bs-btn-* variables (solid + outline, all 8 variants, incl. the
.btn-light/.btn-dark force-overrides), so buttons/badges follow the theme.
Unit tests assert byte parity with Bootstrap's compiled output for every
variant; validated visually with headless Chromium (primary->pink recolors the
button). Opt out with tokens.deepOverlay === false.
The editor's live preview uses POST /api/preview-css (the same server
compiler -> single source of truth), so the preview is WYSIWYG including
recolored buttons (debounced, stale-response-guarded, no save/activate).
Installed in the dev TEST instance
theme-builder is installed in the :3001 TEST instance (registered by location,
loaded on boot via startServerTest.sh). Validated in a real browser (headless
Chromium): the editor manager, the manifest-driven token panel, and the live
preview (navbar/buttons/links recolor) all work against TEST. The install is
non-disruptive: with no active theme and layout mode off, theme.css is a no-op and
sbadmin2 remains the active layout, so the TEST site looks unchanged until an
admin activates a theme.
Rendering engine: overlay + deep overlay (Sass deferred)
The shipping engine is the CSS-variable overlay + deep overlay (no build, no
runtime deps). With the deep overlay recoloring buttons/components, this covers
the practical theming surface. The dart-sass full recompile remains an
opt-in (lib/sassCompile.js degrades to the overlay when sass is absent);
the full Bootstrap-SCSS vendoring is intentionally NOT shipped, since the deep
overlay made it unnecessary for the common case. Enable it later by adding sass
- vendoring
scss/bootstrapif a theme needs build-time-only Sass features.
Plugin vs Theme classification
theme-builder exports a layout function, so Saltcorn classifies it as a
Theme (local_has_theme): it appears under the Plugins page's All / Themes
/ Installed tabs (next to sbadmin2), not the "Modules" tab. This is intentional
and correct (in layout mode it is a layout). The Configure gear renders in every
tab it appears in.
Status (Phase 1)
Built and unit-tested (server-independent):
- Pure core (
node --test, no DB/server):compile.js(overlay compiler),sanitize.js,tokenSchema.js,themeSchema.js,portability.js(export/import + upcasting),builtins/(Flatly/Darkly/Cosmo starters). - Storage, wiring, HTTP, lifecycle, editor:
themeStore.js(the only module that touches the Table),activePointer.js,cfgReaders.js,cssCache.js,activate.js,headers.js,layout.js,routes.js,apiHandlers.js,apiState.js,httpUtils.js,onLoad.js,configWorkflow.js,index.js, plus the buildless editor (lib/page.js+public/builderApp.js).
All *.js pass node --check; the pure suite is green (npm test, 31 tests).
Validated end-to-end against a live SQLite instance (in-process harness + real HTTP), all green:
onLoadcreates the_themebuilder_themesTable (registered in_sc_tables).- create -> save (version-CAS; stale save -> 409) -> activate (pointer +
composite
activeHashpersisted) ->GET /theme.cssserves the overlay. - export -> import mints a fresh id with a de-duped name.
- Activating a built-in serves its overlay (the
getByIdseam fix). - Real HTTP: public
theme.css(200, immutable+ETag), admin guard (redirect for HTML, JSON 401 for the API), authed/api/state, the editor shell, and the overlay<link>?v=<hash>injected into rendered pages.
Reproduce with npm run test:integration (self-contained throwaway DB).
Dependency convention: like
dev-deploy/idp, this plugin declares no@saltcorn/*dependencies -- the host provides them at runtime. Declaring them makes the plugin installer try to fetch/copy them. Phase 1 has no third-party runtime deps at all.
The editor under public/builderApp.js is a buildless, dependency-free
vanilla-JS app — the Phase-1 shipping UI. The React + Craft.js source tree
(builder/src, webpack-built into public/builderApp.js) is the Phase-2
upgrade path and is not yet implemented.
Storage & backup-safety
The theme library is a real Saltcorn Table (_themebuilder_themes)
created via the Table model, so it is registered in _sc_tables and rides
backup / restore / snapshots for free, per-tenant. The active pointer
(activeThemeId, activeByRole, activeHash, layoutMode) lives in
plugin.configuration, which is also captured by plugin_pack. Compiled CSS is
a derived cache, never stored. (This deliberately avoids the raw-DDL
backup-invisibility pitfall documented for the other plugins.)
Running
This plugin is loaded by a Saltcorn instance, not run standalone. Install it as a
local plugin pointing at this directory; onLoad creates the table per tenant
and rehydrates the active-theme CSS cache. The admin editor is served at
/theme-builder/editor; the public stylesheet at /theme-builder/theme.css.
npm test # pure unit tests (no server)
npm run test:integration # in-process e2e against a throwaway SQLite DB
npm run build:ui # layout mode: build the React/Craft.js editor (not yet present)
Security: import model
Theme import performs NO security sanitization of the uploaded file. An admin
can already inject arbitrary site-wide CSS/JS via page_custom_css,
page_custom_html, or by installing plugins, so an admin-only theme import
crosses no boundary the admin role does not already own. Import runs only four
robustness checks (size cap, format/version upcast, shape validation, and a
"compiles to balanced CSS" probe) — these exist so a malformed file fails with a
clear error and cannot white-screen the site, not as a defense against a trusted
admin.
Contingency: if a non-admin role is ever permitted to import or manage
themes, real sanitization (CSS/selector/@import/url()/expression filtering and
layoutTree resolver allow-listing) MUST be reintroduced before that ships.
See lib/portability.js.
License
MIT