Repository navigation
Conversation
The design engine now derives the portal's component tokens from the solved palette and the shape and layout choices, and points the variables portal components already read at them: the header, the menu, the voted colour and the button, field and panel radii. The default design reproduces the values the stylesheet hard-codes today, so a converted workspace keeps its header, menu and cards. - Corners set the control, field and panel radii; density sets the card padding, the gap between cards and the post title size; width sets the page width, which every portal page now reads. - Menu styles: tabs as today, underline under the current page, and pills on the subtle accent. Accent text is now solved against the subtle accent as well as the page and cards, so pills and links stay readable on it. - The voted vote box follows accent text, which the solver keeps at 4.5:1. - Structure travels as data-qb-* attributes on <html>: the header, menu, band, card style and filter side. Raised and list cards and filters on the start side are attribute variants, so legacy pages keep their classes. Computed styles on 16 portal pages are unchanged with the flag off. With it on, the default design differs only in contrast fixes.
The feed card, the post page and similar posts, the widget and the embed card each built their own vote box classes, and they had drifted: the embed was narrower, kept its count in the ink colour when voted and had no hover edge. They keep their own behaviour and now take their look from voteBoxClasses(), so there is one vote style. The embed card gains the standard size and voted count.
The snapshot keyed each element by its index among all its siblings, scripts and style elements included, so moving a style element between the head and the body shifted every later path and read as thousands of differences. Paths now count only the elements the snapshot walks.
The portal layout wrote its theme and custom CSS as style elements in the body, and the access gate wrote them again. The layout's head() now emits both, after the app stylesheet so they still override it, read from the settings the root context already carries. The portal loader no longer sends a second copy of the theme CSS, and the gate no longer injects its own. The widget and the portal auth pages keep PortalDesignStyle. Computed styles on 16 portal pages are unchanged, with the design engine off and on.
The design engine reads one registry of bundled fonts, with each family's label, category, stack and the weights its stylesheet loads. Today's families keep their stacks. IBM Plex Sans, Fraunces and Bricolage Grotesque join them, each in its own stylesheet that loads only when a design uses it. A design's heading font now reaches the portal: headings read --qb-font-heading from the base layer, so a utility on a heading still wins, and the heading weight beats the stylesheet's unlayered heading rule. The body font follows --font-sans without the !important rule the legacy theme needs. Portal pages and the widget load the design's body and heading families by id, so a family outside the legacy list loads too. Computed styles on 16 portal pages are unchanged with the engine off and on, and the widget stays within its bundle budget.
The actions behind every surface read the design being edited (the draft, else the published design, else the branding settings converted on the spot), save and discard the draft, publish it, list versions and restore one. Each refusal a caller must explain (an invalid or unreadable design, a stale revision, an overtaken draft, nothing to publish, a missing version) comes back as a value with its code. - Server functions for Settings › Portal, each gated on the branding permission. Publish and restore record the request in their audit rows. - REST: GET and PATCH /api/v1/settings/portal_design (a JSON merge patch onto the draft, with If-Match), DELETE .../draft and GET .../versions, documented in OpenAPI. - MCP: get_portal_design and update_portal_design_draft. A key or an agent edits only the draft. Publishing and restoring stay with a person in Settings › Portal, where the draft shows what is waiting, so no change reaches visitors without one. The authorization matrix, the domain graph and the MCP tool counts are regenerated.
The MCP scope map now lists get_portal_design and update_portal_design_draft, so the draft tool is audited as a write, and the REST route words include portal_design, draft and versions, so their audit rows keep the route's name.
Embed cards render inside the admin inbox, and importing voteBoxClasses there split the helper into a chunk of its own that the inbox then requested. The embed card keeps its own copy of the classes, and a test holds it equal to voteBoxClasses in every state, so there is still one vote style. The inbox loads exactly the chunks it did before.
mortondev
force-pushed
the
feat/portal-design-v2-phase2-foundations
branch
from
October 30, 2026 20:19
2082323 to
92ed776
Compare
A shared vote box helper sits in different route bundles, so the bundler splits it into a chunk of its own, and moving it only moved the extra request: first to the admin inbox, then to the roadmap. The four vote boxes keep their own classes, as before. The embed card's are brought into line with the others (the standard size, the voted colour on the count, the hover edge and the chevron lift), so visitors see one vote style. The inbox, the inbox-to-roadmap navigation, the widget and the portal load exactly the chunks they did before this stack.
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The third of the stack that builds a new design engine for the portal. This one gives the engine everything the Appearance view in the next PR builds on: component tokens and layout attributes, one vote box, the styles in the document head, one font registry with three new families, and the API for editing a design. Behind the
portalDesignV2flag, which stays off by default. With the flag off nothing a visitor sees changes, apart from the embed card's vote box, which now matches the others.Component tokens and layout attributes
data-qb-*attributes on<html>: the header, menu, band, card style and filter side.One vote style
The embed card's vote box had drifted from the others: it was narrower, kept its count in the ink colour when voted, and had no hover edge or chevron lift. It now matches the feed card, the post page and the widget. Each keeps its own classes. A shared helper sits in several route bundles, so the bundler split it into a chunk of its own and every page with a vote box paid one more request. The admin inbox, the inbox-to-roadmap navigation, the widget and the portal load exactly the chunks they did before this stack.
Styles in the head
The portal layout wrote its theme and custom CSS as style elements in the body, and the access gate wrote them again. The layout's
head()now emits both after the app stylesheet, read from the settings the root context already carries. The portal loader no longer sends a second copy of the theme CSS. The widget and the portal auth pages keepPortalDesignStyle.Fonts
--qb-font-headingfrom the base layer, so a utility on a heading still wins. The heading weight beats the stylesheet's unlayered heading rule.--font-sanswithout the!importantrule the legacy theme needs.Editing a design
One set of actions sits behind every surface. They read the design being edited: the draft, else the published design, else the branding settings converted on the spot. They save and discard the draft, publish it, list versions and restore one. Each refusal a caller must explain comes back as a value with its code: an invalid or unreadable design, a stale revision, an overtaken draft, nothing to publish, or a missing version.
GETandPATCH /api/v1/settings/portal_design(a JSON merge patch onto the draft, with If-Match),DELETE .../draftandGET .../versions, documented in OpenAPI.get_portal_designandupdate_portal_design_draft.A key or an agent edits only the draft. Publishing and restoring stay with a person in Settings › Portal, where the draft shows what is waiting, so no change reaches visitors without one.
Harness
Style snapshots now key elements only by the elements they walk, so moving a style element between the head and the body no longer shifts every later path.
Verification