sc-theme-builder/README.md
2026-07-01 20:07:28 -05:00

158 lines
8.2 KiB
Markdown

# 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 `<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`.
```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