# 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`](./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 `?v=` 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`. ```sh 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