Skip to content

feat(design): the workspace's own CSS, checked as it is saved - #854

Closed
mortondev wants to merge 1 commit into
feat/portal-design-v2-phase5-partsfrom
feat/portal-design-v2-phase5-css
Closed

mortondev wants to merge 1 commit into
feat/portal-design-v2-phase5-partsfrom
feat/portal-design-v2-phase5-css

Conversation

@mortondev

@mortondev mortondev commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

The twenty-first of the stack that builds a new design engine for the portal, and the second of phase 5. A workspace can write its own CSS over the design, styling the portal's parts by name, and the CSS is checked as it is saved. Behind the portalDesignV2 flag.

Custom CSS in Sections

A closed "Custom CSS" disclosure at the end of Sections, with the same editor the legacy theme's Advanced CSS uses (its lazy chunk is shared). A line names the parts to style. The CSS goes into the design's css field and renders in the tenant layer, so it wins over the design without !important. The preview shows it before publishing, and Review and publish counts it in lines rather than showing it.

The check on save

lib/server/domains/design/css-lint.ts:

  • Refused, as a refusal value with the line: @import; url() anywhere but the workspace's own storage (/api/storage/…, so data: and other hosts too); expression(); behavior; and <.
  • Warned, in the save's report:
    • an internal class name, with the part to use instead, which "Fix it for me" swaps in. It only swaps whole class names, so .portal-nav__item leaves .portal-nav__item--active, and longest first;
    • !important;
    • setting a --qb- token no part reads;
    • a part that does not exist, or a renamed one (with its swap);
    • text and background colours on one part that do not reach 4.5:1.
  • rewritePartAliases() runs when the design is read, so CSS naming a renamed part keeps working; it is idempotent.

It parses with css-tree, pure JavaScript, rather than lightningcss, so the server bundle carries no native module. It imports css-tree's self-contained build (css-tree/dist/csstree.esm): the module entry reads data files at runtime, which the server bundle does not carry, and every request then failed as the design service loaded. A test pins the import.

Plan

Custom CSS stays a plan feature (customCss), as it is for the legacy theme. Saving new or changed CSS asks the plan; CSS the published design already has stays, and removing it is always allowed, so a workspace whose plan dropped the feature can still save everything else. The REST API answers 400 for refused CSS and 403 for the plan.

Verification

  • On the local Kiln workspace, with Playwright: an @import was refused with its message; CSS styling a part with an internal class was saved with a warning, and "Fix it for me" swapped in [data-qb-part="nav-item"]; the preview applied the CSS (3px card borders); review counted it; after publishing, the live portal had 3px card borders and bold menu items. No page errors.
  • New tests, each checked by breaking the code it covers: each refusal and warning, the parse error with its line, the alias rewrite moving an old name once and only once; refused CSS as a refusal through the design actions, warnings in the report, and the plan gate asked for new CSS but not for published CSS or its removal; the editor writing and emptying the draft's CSS, and "Fix it for me" swapping whole names only; review naming the CSS.
  • The core bench holds every ceiling, and so does Portal settings. The parser stays on the server, so no admin or portal chunk grows.

@mortondev
mortondev added this pull request to stack #833 October 31, 2026 06:08
A workspace can write its own CSS over the design in Sections, styling the
portal's parts by name. It renders in the tenant layer, the preview shows
it, and review counts it in lines.

The check on save refuses @import, url() anywhere but the workspace's own
storage, expression(), behavior and <, as a refusal with the line. It warns
on internal class names, offering the part to use, which Fix it for me
swaps in, and on !important, tokens no part reads, unknown or renamed
parts, and text and background colours on a part below 4.5:1. Old part
names are rewritten as the design is read.

It parses with css-tree's self-contained build, pure JavaScript with its
data inlined, so the server bundle needs neither a native module nor data
files beside it.

Custom CSS stays a plan feature: new or changed CSS asks the plan, while CSS
already published stays and removing it is always allowed.
@mortondev

Copy link
Copy Markdown
Member Author

Condensed into #879. The stack is now five PRs, #878 to #882; this branch is kept as it was.

@mortondev mortondev closed this Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant