158 lines
8.2 KiB
Markdown
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
|