Skip to content

feat(design): switch a portal with its own CSS, and back - #855

Closed
mortondev wants to merge 2 commits into
feat/portal-design-v2-phase5-cssfrom
feat/portal-design-v2-phase5-switch
Closed

mortondev wants to merge 2 commits into
feat/portal-design-v2-phase5-cssfrom
feat/portal-design-v2-phase5-switch

Conversation

@mortondev

@mortondev mortondev commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

The twenty-second of the stack that builds a new design engine for the portal, and the third of phase 5. A portal with its own CSS stays on its old theme until someone switches. This adds the switch, with today's portal beside the switched one and a report on the CSS, and the way back. Behind the portalDesignV2 flag.

The card

With the engine on, the old Settings › Portal page shows one card above today's settings: "Your portal uses custom CSS. Switch to the new design system." It has:

  • Compare: today's portal and the portal switched, side by side in two preview frames.
  • The CSS report, in three lists:
    • Fix in Advanced CSS before switching: what the CSS check refuses. Switch waits until these are fixed.
    • Moves into the design: theme variables the CSS sets on :root or .dark that become design fields, such as --primary becoming the accent and --radius the corners.
    • Check after switching: theme variables the design now sets instead (such as --ring), internal class names with the part to use, and the CSS check's other warnings.
  • Switch: publishes the design and reloads the page as Appearance.

Once switched, History has "Switch back", with a confirmation, since visitors see it straight away.

What switching does

lib/server/domains/design/switch.ts:

  • The design is today's branding converted, as for a portal without CSS, with the CSS's :root and .dark theme variables read in over the stored ones first. The design sets the portal's theme variables on <html> outside any layer, so a :root variable in the tenant layer would otherwise stop applying without a word.
  • The CSS carries over unchanged into the design's css, rendering in the tenant layer. It is the portal's CSS already, so the plan is not asked. The check still refuses what reaches past styling, here as before.
  • The old theme's settings stay untouched. Switching back unpublishes the design (publishedRevisionId to null), so the engine draws the old theme again; the design's versions and any draft stay, and the switch can be made again.
  • Both write audit rows, design.switched and design.switched_back, with filters in the audit log.
  • Nothing to switch (no CSS beyond comments, the flag off, or PORTAL_DESIGN_ENGINE=legacy) is a NOT_SWITCHABLE refusal; switching back with nothing published is NOT_SWITCHED.

carriesThemeVariable() in the converter answers whether it would carry a value, so the report and the conversion cannot disagree: colours that parse, corners in rem, px or em, and a font the design bundles, the last two from the light theme only.

.post-card now gets its part as a fix in the CSS check, since the class sits on the post-card part's element; the switch round on a real portal showed it was missing.

The switched frame

The portal draws quite differently on each engine, so the frame cannot fake the design over a portal drawn by the old theme. With ?design=switch, the root asks the bootstrap for the portal drawn as it would be after switching, and the bootstrap gives it only to a dashboard session that can edit branding; anyone else, or a portal with nothing to switch, gets the settings unchanged. The portal keeps the flag as the frame navigates, like theme and preview.

Verification

  • On a local workspace on its old theme with hand-written CSS (:root and .dark variables, internal classes), with Playwright: the card offered the switch; the report moved --primary and --radius into the design and warned about --ring and the internal classes; the left frame drew the old theme and the right one the design with the CSS (accent, sharp corners, the 3px header rule); visitors kept the old theme until Switch; then the page became Appearance and visitors saw the design with the CSS; History's "Switch back" returned the old page with the card, and visitors saw the old theme again. No page errors.
  • New tests, each checked by breaking the code it covers: the report, the switched preview only before switching, publishing with the CSS and its variables without asking the plan, switching back untouched and switching again with audit rows, refused CSS holding the switch, and nothing to switch without CSS, without the flag or under the override; the bootstrap drawing the switched portal only for someone who can edit branding; the card's lists, frames, Switch and refusal; Switch back offered only after a switch, and confirmed first; which theme variables the converter carries.
  • The core bench holds every ceiling, and so do Settings › Portal and the admin pages closest to their limits. Each page's scripts grow by about 0.1 KB (the bootstrap option and the portal's search flag); the card adds to Settings › Portal's own chunk.

A portal with its own CSS stays on its old theme until someone switches.
Its old Settings › Portal page now offers the switch: today's portal beside
the portal switched, a report on the CSS, and Switch, held while the CSS
has refusals. History offers the way back.

The switch publishes today's branding converted, with the theme variables
the CSS sets on :root and .dark read in first, since the design sets those
variables itself and a tenant-layer :root would stop applying. The CSS
carries over unchanged without asking the plan, as it is the portal's
already. Switching back unpublishes the design, so the old theme, never
touched, shows again; the design's versions stay. Both are audited.

The switched frame is drawn by the engine on the server: with
?design=switch the bootstrap draws the portal switched, for a dashboard
session that can edit branding only.

The CSS check now offers the post-card part for .post-card.
@mortondev
mortondev added this pull request to stack #833 October 31, 2026 06:53
@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