Theme builder plugin for Saltcorn.
Find a file
2026-07-01 20:07:28 -05:00
builder Initial commit 2026-07-01 20:07:28 -05:00
lib Initial commit 2026-07-01 20:07:28 -05:00
public Initial commit 2026-07-01 20:07:28 -05:00
test Initial commit 2026-07-01 20:07:28 -05:00
.gitignore Initial commit 2026-07-01 20:07:28 -05:00
ARCHITECTURE.md Initial commit 2026-07-01 20:07:28 -05:00
index.js Initial commit 2026-07-01 20:07:28 -05:00
package.json Initial commit 2026-07-01 20:07:28 -05:00
README.md Initial commit 2026-07-01 20:07:28 -05:00

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/bootstrap if 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:

  • onLoad creates the _themebuilder_themes Table (registered in _sc_tables).
  • create -> save (version-CAS; stale save -> 409) -> activate (pointer + composite activeHash persisted) -> GET /theme.css serves the overlay.
  • export -> import mints a fresh id with a de-duped name.
  • Activating a built-in serves its overlay (the getById seam 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