Repository navigation
Conversation
The builder will offer the match until one has run, rather than until the design holds a logo or colour (onboarding's logo alone is not a matched brand). Stamped by the brand match on the design it returns.
… build does not know After a rollback, a stored design can carry an option a newer build added to an optional field (bandGlow, bandShape, shape.hover and others). The default design has no value at those paths, so repair gave up, readStoredDesign returned null, and the portal lost its whole brand, published and draft alike. An optional field that fails validation (unknown value or wrong kind) is now dropped on its own and recorded in `dropped`; the rest of the design is kept. Required fields still fall back to the default design's value as before. The test is generated from the schema: every optional enum field gets an unknown value and must read with only that key dropped.
The portal is a feedback board: visitors write posts. The design AI's few-shot examples and prompt text taught it to say "ideas", so generated hero headlines, subtitles and button labels did too. The examples and prompt now use posts and feedback, and the prompt says so outright. A deterministic step (words-posts.ts) also rewrites English "idea" and "ideas" in the words the AI returns (hero and feedback headline, subtitle, prompt, submit button) to post and posts, with "an idea" becoming "a post". Other languages are untouched. A golden test covers it, plus a guard that no example teaches "idea". Designer preview text and one block description no longer say ideas. The English catalogues were already clean and the default composer button is already "Write a post".
One click on a preset look replaced brand.light, brand.dark and the fonts, which undid a brand match, and the tiles and hover previews drew each look in its own palette rather than the workspace's. A look now gives only its treatment (corners, layout, hero, type scale, case and exact values). withLook keeps the draft's palettes, tuned colours, faces, heading weight and mode by default, reusing keepOwn, which moves from presets.ts to looks.ts so both paths share it. Passing itsColours takes the look's colours, fonts and mode as well; applyPreset keeps its old whole-look behaviour through it. Tiles draw the brand with the look's treatment, from the resolved design so the contrast solver picks the hero letters, with the workspace logo in the band. Each tile offers "Use its colours too" on hover or focus, applied as one step labelled "Look and colours: <name>"; a plain choice is labelled "Look: <name>".
…atch The launch plan's branding step stopped at a logo, linked to Settings > General and was done once a workspace logo existed. On a portal drawn by the design engine that logo is not the portal's brand, and website branding accepted from onboarding wrote only the legacy branding settings, which a published design ignores, so it silently changed nothing visitors saw. - The step is now "Make the portal look like your brand". In the builder it links to /design?match=<domain>&from=onboarding (domain when the logo lookup knows it), and on the legacy theme it keeps the logo page. - The builder handles ?match on arrival (useMatchOnArrival): it runs Ask AI's own brand match into the draft, with its single Undo, then clears the address so a reload does not rerun it. Without AI, or without a website, it opens Theme on the Brand kit. /design already requires SETTINGS_BRANDING. - The step is done when a brand match has run (brand.matchedAt on the published design or the draft) or the Brand kit has an icon, a brand colour and a font that differ from the defaults; a logo alone no longer counts. Legacy portals keep the workspace logo rule. - Accepting website branding also writes the logo and colour into the design draft once a design is published, keeping an icon or colour the design already has. - Settings > General labels the workspace logo "Admin and emails" once a published design carries the portal's own brand.
…n the Brand kit
"Match my brand" hid itself as soon as the design held any logo or colour,
and onboarding maps the workspace logo into logo.mark, so onboarded
workspaces never saw the fastest way to brand the portal. The offer now
depends on whether a brand match has ever run (brand.matchedAt).
The stamp moves from brandMatch into runBrandMatchRequest, so every path
(screenshots, a capture, the site's code alone) records it on the draft it
saves and on the design the builder adopts.
Theme > Brand kit gains a first row, Match your brand: a website field
prefilled from the workspace's known domain and a drop zone for up to three
screenshots. It runs the same match as Ask AI through a new reusable entry
point, useDesignAi().startBrandMatch({ website, screenshots, from }), so the
reply, the check and its single Undo show in the conversation. Once a match
has run the row stays as a quiet "Match again".
Without an AI key there was no way to bring the brand in from the brand's
own site, though the site reader and its extractors need no model. When AI
is off, the Brand kit's Match row becomes "Use from your website": it reads
the site from its code, stores what it gives, and offers checkable chips
for the logo, favicon, share image and one brand colour. The checked ones
go into the draft as one undo step that also records brand.matchedAt, so
the row turns quiet ("Use again") like a match. Font names are left out:
a site's code names them too unreliably to offer.
The read is a new server function, readSiteBrandFn, gated on
SETTINGS_BRANDING, returning refusals (not a website, unreadable, nothing
found) as values. The authz matrix golden is regenerated for it.
…enu hint The first-open coach mark explained the menu, sat over the picked part, ignored Escape and never mentioned the brand. While no brand match has run, the first open now shows Theme with a coach mark under the Match your brand row: "Paste your website to match your brand". It sits in the panel's own order so the keyboard reaches it, and closes on Got it, Escape, or once a match or site read starts. The menu hint shows only while the menu is hovered or the header picked, sits under the whole header so it never covers the picked part, closes on Escape (from the builder or the canvas), and says one line: "Pick an item to rename or move it." Coach marks share one remembered-per-person hook.
A logo upload never suggested a brand colour: logo colours were filled
only by hand, from a row at the end of Colours labelled for status
markers. Uploading an icon or a full logo now reads its colours with its
ink and stores them as the logo colours in the same undo step, and the
Brand colour row shows them as "From your logo" swatches. Picking one sets
the brand colour as one named step ("Brand colour from your logo"); the
colour already in use is not offered again.
…callback useMatchOnArrival now only decides what an arrival link asks for and clears the address; the match itself comes in as start(website), so the builder's shared brand match entry point can be passed in at merge time without touching the hook. The shell passes Ask AI's site match for now.
Fonts are the most common miss in a brand match, and the reply told people to "upload your own fonts in Brand", a panel that does not exist: the upload lives in Theme > Brand kit > Fonts. - The note now names the real place: Theme > Brand kit > Fonts. - When the brand's site names a face no bundled face is (drawn in the nearest instead), the match result carries it (font: family and slot, both slots when the site sets one face throughout), unless the design already has that family uploaded. - In the builder the reply card gets "Upload <family>", which opens Theme with the Brand kit's font upload open and its name and slot filled in, and a plain "Fonts in Theme" link to the Brand kit's fonts. The reply itself stays one line with its one Undo.
Onboarding's arrival match runs through the Brand kit's startBrandMatch, so there is one match path; GRAPH.md regenerated for the new imports.
This was referenced Nov 7, 2026
This branch has not been deployed
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.
Wave 1 of the portal builder critique: make the brand stick and make it easy to find. Stacked on #922.
What changes
brand.matchedAt. Before, it was hidden as soon as a logo existed, and onboarding always adds a logo./design?match=<domain>&from=onboarding, which runs the brand match into the draft. It counts as done once a match has run, or once the Brand kit has an icon, colour and font.Notes
Testing
/design?match=example.com&from=onboarding.