Skip to content

feat(design): designs that match a brand closely, and an accessible portal - #881

Draft
mortondev wants to merge 46 commits into
feat/portal-design-v2-3-ai-brandfrom
feat/portal-design-v2-4-fidelity
Draft

mortondev wants to merge 46 commits into
feat/portal-design-v2-3-ai-brandfrom
feat/portal-design-v2-4-fidelity

Conversation

@mortondev

@mortondev mortondev commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Designs get close to a brand, measured against its own site, and the public portal meets WCAG 2.2 AA throughout.

In this PR

  • Quackback AI starts from a site's own wordmark and share image, reaches every design choice, writes each look in its own call and mends a look's gaps once
  • Exact values under the design's choices: headline and heading type, buttons, cards and fields, with readable colours guarded
  • The website start measures a brand's type, shapes and colours from its HTML and CSS: roles read from intent, a cascade over the page's element tree, inherited properties and the hero a visitor sees
  • Every page's title follows the design; headlines can take their own face; primary buttons can take the text colour
  • Home's blocks work as the pages they stand for; every card takes the design's card style; roadmap cards vote
  • Accessibility across the public portal: focus, target sizes, names and contrast

Replaces

#868, #869, #870, #871, #872, #873, #874, #875, #877

Fourth of five stacked PRs. The code-scanning thread on #877 (incomplete sanitisation in the reader) is fixed in the fifth PR.

…mage and every new choice

Starting from a website now brings its header logo: the element the page
marks as its logo, or its first link home, as the inline SVG or the
sharpest dark-on-light image inside it. An inline SVG takes the colours
its page gives it, from the site's CSS and its brand colour, so it does
not draw in black once copied out. It is stored as the design's wordmark
when it draws clearly wider than it is tall and holds nothing that runs,
with its own colours for status markers. The site's share image is
stored when it is a photo large enough for a band, and the model, which
sees it, decides whether it suits one. Files are named by their content,
and a look may place only the files stored for its start.

The three looks now choose every new setting: a second colour, support
and blend bands, the band's alignment and shape, a header on the band,
headline scale, tracking and case, an accent phrase, button shape,
status style, how the logo shows, and where Home's composer sits. The
design system says when each fits, and the edit tool's field guide
covers them, so a person can ask for any of them.
The check for an SVG file skipped its XML declaration and comments with a
regular expression that could backtrack on a run of comments. It now
steps past them in a loop, and a fetched SVG is stored from its markup's
start.
…block can take a row

From the first look Quackback AI built from a website: tight tracking
crowded heavy section headings, so it now applies to the page headline
alone, a touch looser. A contact block can now be full width, which lays
it out as one row, and with the composer in the band a look gives it
that row rather than standing it beside a list it cannot match.
Brand: what the header shows, with wordmark uploads for light and dark
(SVG included); a second colour; a type style row (quiet, confident,
display, poster) that sets headline size, tracking and case together,
with a switch for capitals on the menu and buttons; and buttons' own
shape. Sections: a header on the band, the band's blend, words, shape
and image with where it sits, a phrase of the headline to set apart, and
status markers, with logo colours offered only when the logo has some.
Home: whether the composer sits in the band, which gives a contact block
its own row. The change list names every new field.
The admin consistency check keeps palette colours out of admin screens.
…wn model

From the first round of designs Quackback AI built from websites. The
faithful look now keeps the site's own colour where the model kept its
hue but darkened or lightened it, as models do to make text read, which
the portal does itself. The design system says colours read from a
site's CSS beat colours seen in a screenshot, which a cookie banner can
dim; that a dark site takes a solid band, never a fade; and that a brand
whose headlines are in its colour sets the whole headline apart. A logo
drawn in the page's current colour takes the colour the site's CSS gives
its own classes, else the site's most vivid colour, or near-black for a
monochrome brand.

Designing from a brand asks a lot of a model, so Appearance's AI can now
have its own, AI_DESIGN_MODEL, and uses the assistant's without it.
AI_DESIGN_MODEL joins the AI configuration keys the generated platform
schema describes.
…s legible

A tint band, and the top of a fade, are now a pale tone of the brand
colour at the page's lightness, rather than a mix of the two, which
turned a deep colour over a warm page grey.

A wordmark imported from a website records its ink when it is one colour
throughout. Where that ink would vanish into the header, such as a dark
wordmark on a dark band or in dark mode, the header redraws it white, or
black for a light one. The website reader also reports the colour a site
fills its hero sections with, where its CSS names them.
…econd colour at work

To reach the looks the end-state renders show:

- A name set as the wordmark can be in the brand colour, in spaced
  capitals, and have a part of it in the brand colour, as a two-tone
  wordmark does. On a band it takes the header's text.
- Page headlines can be in the brand colour, the band's accent, which is
  kept readable on the band.
- Cards can be filled: no edge, their own colour on the page, larger
  corners.
- Home's composer in the band follows the card style: raised casts a
  shadow, the rest sit flat.
- The band's call to action can take the second colour, and the second
  colour can tint the vote box and the current menu item, with text kept
  readable on each.
A band without an image can carry quiet art drawn in the brand colour and
the second: hills or waves along its bottom, or dots, a grid, lines,
petals or confetti tiled faintly behind the words. An image behind the
words takes its place; art beside a side image stays.

A band image can be cropped toward its left, centre or right, so a share
image with words down one side shows its picture.
The looks now choose band art, the headline's colour, the name's colour,
capitals and accent part, filled cards, the second colour's tint and the
band's call to action, and where a share image keeps its picture: a share
image with words down one side is used, focused on its picture. With a
Home headline, the composer always sits under it, and the faithful look
keeps the site's exact second colour as it does the first. Appearance
offers each choice, and the change list names them.
…here is a mark

From round three of designs built from websites. A side image focused
left or right now crops to a portrait in a narrower column, so a share
image with words down one side shows its picture alone, and the design
system keeps images with words or logos out from behind the headline.
The contact block takes its row whenever the composer goes into the
band, as a Home headline puts it there. The looks hear when the workspace
has a logo mark, and show it beside the name; the name's colour, capitals
and accent part now style the name beside a mark too.
A highlighted phrase mixed the second colour into the band, which left a
pale one, such as a cream highlighter, all but invisible. A pale second
colour is now used as it is; a deeper one is still softened.
From an axe audit of every public page in ten branded portals:

- Secondary text no longer fades below its colour: the solver keeps it at
  4.5:1, and the faded copies fell short on dates, counts and meta.
- Tag chips keep their colour as a tint, with text in the ink.
- A headline in the brand colour is set by an attribute, so it reads on a
  panel in the band as well as on the band itself.
- Post titles in the feed are second-level headings under the page's.
- The roadmap's sort control is named.
- Home's help block opens the help centre from a labelled search field,
  in place of a disabled button.
- A mark beside the name is decorative; on a phone the name stays
  available to screen readers.
- Share an idea is the feed's composer in the brand's composer shape. It
  opens in place with the board, details, sign-in and submit, posts to the
  first board the visitor can post on, and goes to the posted idea.
- Ideas vote by the feed's rules: the board's, and the viewer's.
- Help search searches the help centre as you type, with Ask AI where the
  help centre offers it; its field takes the design's composer corners.
- A board default that arrives after the composer mounts is taken while
  none is chosen.
…ards vote

- A card surface is marked once, and the design CSS gives every one the
  design's card style: raised, as rows, filled or bordered. The feed, Home,
  the post's sidebar, the help centre, support, status and the welcome card
  now follow it, in place of three copies of the same classes.
- Roadmap cards vote by the board's rules. The title's link covers the
  card and the vote sits above it, so neither is inside the other.
- The parts list names the help search field, and the composer and vote
  where they are now drawn.
…lain

Roadmap, Changelog, the help centre and Support open with a title in the
design's headline type, where each had its own. A design sets them on the
band as Home's headline is (layout.pageTitles) and can word each one
(pages.titles), with an accent phrase like any headline; a page left
unworded says what it usually does. On the design engine the help centre
drops its own grid and glow for the design's.

Appearance has a Page titles section, Quackback AI can set and word them,
looks choose band or plain from the brand's site, and the preview shows a
draft's titles as they are typed.
- A menu row wider than the screen fades at an edge with more beyond it,
  and scrolls the current page into view, where the end of the menu was
  cut off with no sign of more.
- Roadmap columns share the page's width, where a few narrow columns left
  most of it empty. Their dots follow the design's status markers.
- Tags take the status markers' style: tinted with their colour, in the
  brand's colours where the design maps statuses to them, and plain where
  statuses are in the ink. The changelog's categories read in the ink.
…band short

- brand.fonts.display sets the page headlines in their own face, such as a
  condensed poster face, while section headings keep the heading font.
  Appearance offers it beside the heading and body fonts, Quackback AI can
  set it by name, and looks choose one when a brand's hero type differs.
- A page title on the band keeps it short: no image beside or under the
  words, and less room around them. A page's own buttons there take the
  band's colours, so they read on it.
- The roadmap's note on its height is a comment again, not page text.
…Support sits on a card

- A post's title on its own page takes the design's page title size, held
  short of it for a long title. It is a part, post-heading, for a design's
  own CSS.
- Signed out, Support's way in sits on a card in the design's card style,
  where it floated alone on the page.
…s headings step down one level

From the second accessibility pass over the branded portals:

- The status filters' counts were faded below the contrast the solver
  keeps; they now read in the pill's own colour.
- An empty state titles itself at the level it is given. Support's sit
  under the page's title, so they are second-level headings.
…acing follow the design

From the second brand critique:

- A page's own buttons on the band inherit its ink, and centre with its
  words; the changelog's RSS button was dark on a dark band.
- Every status and tag dot is drawn by one component that follows the
  design's status style: the feed's filters, similar posts and roadmap
  columns as well as badges.
- Where page titles are on the band, a post opens on a strip of it with
  the way back to its board, where it opened on a plain page.
- The roadmap's description is a line under its tabs, not a card around
  a few words; the help centre has room under a band title.
- The parts contract covers parts drawn by routes as well as components.
Many brands keep their colour for the logo and links and fill their main
buttons in black or their text colour. shape.fill sets primary buttons in
the brand colour or the text colour; a button now names its variant, so
the design colours primary buttons and leaves the others alone.
Appearance has a switch for it, Quackback AI can set it, and looks choose
it from a brand's own buttons.

The status drawing in Appearance uses the semantic colours.
A brand sits between the tiles' steps: its headline 48px at weight 300,
its buttons 40px tall with 4px corners. A design's tune sets exact values
for headlines, section headings, labels, buttons, cards and fields, each
bounded so no value can break a page, and each one left out keeps what the
tiles choose. Headlines take their own weight, defaulting to the heading
weight; a headline's size scales down on a phone. Section headings are a
part, so a tune sizes them without touching smaller headings.

tuneFromMeasurements turns what a reader measured on a brand's own pages
into exact values a person would choose, rounding to steps and holding a
site's hero-sized section headings to what a card can carry.
The reader finds a page's headline, a section heading, its main button, a
card and a field in its HTML, and applies the CSS rules that match them in
cascade order, utility classes included, with rem, clamp(), min(), max(),
vw and the page's custom properties resolved as a wide, light screen sees
them. Rules for states, theme scopes, dark themes and small screens are
left out, and what cannot be read stays unmeasured. No browser is needed.

The faithful look takes what was measured as exact values, and every look's
brief says what was measured so its steps start near it. A tracking
measured as none is kept as none.
A tune can colour headlines and section headings and fill primary buttons
with their own label, weight and case, in light mode. Each colour is
checked where it sits: a headline at 3:1 on the page and the band (and not
at all across a blend or an image), a section heading at 3:1 on the page
and a card, and a button's label at 4.5:1 on its fill, else the fill's
best label. A colour that would not read is left out.

The website reader measures the headline's and heading's colours and the
main button's fill, label, weight and case, and the faithful look takes
them.
From a keyboard sweep of every public page on ten branded portals:

- Anything focused by keyboard gets a ring in the solved focus colour, in
  the base layer, so a component without its own focus style still shows
  one; the vote box's own ring replaces the browser's, which vanished on
  a dark page.
- A card style's shadow gives way to a focus ring, which is drawn as one;
  linked help cards on filled designs lost their ring.
- Small targets reach 24px without changing their look: tags, author
  names, the composer's sign-in link and Home's roadmap links.
- The composer and the help search show a full-strength ring while typed
  in, and author names a ring as well as an underline.
- The logo link is named for whose portal it is when it shows a wordmark
  or a mark alone.
- A section heading's own colour must read at 4.5:1, as it shrinks to body
  size on a phone.
From the second keyboard sweep:

- The rich text editor is a multi-line text field named by its
  placeholder, where it had no name for a screen reader.
- The composer rings itself with an outline while typed in, which its
  animated shadow cannot draw over.
- The feed's search field fills its pill, so the whole pill is the field,
  and rings at full strength.
- A page's own buttons on the band ring in the band's ink, where the
  page's grey ring was faint on a dark band.
- Home blocks' links reach a 24px target without moving their line.
…led faces

From a review of the branded portals' typography:

- A brand's measured section headings keep its face, weight, tracking and
  colour but take a card title's size: 18px, a little more the larger the
  brand sets them, never past 24px, with tight tracking eased. A site's
  h2 introduces a screen; here it titles a card over 14-16px content, and
  at the brand's own size it outweighed everything around it.
- A headline's line grows with it, on the tiles' scale and with an exact
  headline size, so it never reads as a caption under a poster.
- Eight grotesk and geometric faces carry a light weight, for brands whose
  headlines are set light; only workspaces that choose them load it.
- nearestBundledFont maps a brand's face to itself when bundled and to the
  bundled face of its class and build when it is a well-known brand face.
  The faithful look takes them, and every look's brief names them.
A stack's first known family wins, so a brand face ahead of a Helvetica
fallback is matched as itself.
system-ui, sans-serif and the platform defaults count only when they lead
the stack, where a brand chose them.
…eave the page room

A light weight that carries a brand's 32px heading is too faint at a card
title's size, so a measured section heading is at least weight 400. A
measured headline is held under 88px: a portal's headline leads a page
of content, not a marketing screen.
Brand has an Exact values section, folded away until wanted: the
headline's size, weight, spacing, line height and colour; section
headings' size, weight and colour; buttons' corners, height, fill and
label; cards' corners, edge and shadow. A field left blank keeps what the
choices give, each value is held within its bounds, and one button clears
them all. The review list names them. The colour field is shared with the
rest of Brand.
… faithful look takes the site's main button

From judging the AI's portals against the approved goals:

- The headline is the largest h1 in sight, past hidden, screen-reader-only
  and notice elements; a size set only by a reset (h1 { font-size: 2em })
  says nothing about the brand and is left unmeasured.
- The main button is the one filled with the site's own button colour,
  where a secondary button came first in the page.
- The faithful look takes a coloured main button as its brand colour, and
  fills buttons with the text colour where the site's are near-black.
…very look words every page

- When a site fills its hero with a colour, the faithful look's band
  takes it: the brand colour when the hero is that colour, else the
  hero's colour as the second.
- Looks write the roadmap's, changelog's, help centre's and support's
  titles and lines in the brand's voice, where every page said its own
  name; the edit tool writes a page's title from plain text, a line at a
  time, as it does a headline.
- The faithful look's rules, applied after the model's choices from what
  was read and measured on the site, live in their own module.
…compiles

A look is answered under a strict schema the provider compiles into a
grammar, and past a size it refuses the request: every start then fell
back to an unbranded portal. The page words are now a flat list of eight
lines, and faces are named as plain text and checked after, a face that
is not bundled being left out. A test holds the schema at the size that
compiles, so a new field is flattened or asked for separately rather than
growing the look.
… only its colours

Asked to match a screenshot, the edit chat saw only the picture's colours,
so it could not read the layout, type or shapes it was asked to match. An
attached image now goes to the model as an image, downscaled as the looks'
start does, with its colours alongside. An answer to match a picture can
touch many fields, so it has more room; one that does not fit is asked for
once more, then without the picture.
…ounts the styles its page uses

- A site that bundles its styles into one sheet can ship it at a few
  megabytes; the reader dropped any sheet over 1 MB, so a 2 MB bundle went
  unread and the reader measured whatever sheets were left. It now reads
  up to 4 MB a sheet.
- A bundle carries styles for screens the page is not showing. A rule
  whose classes the page's HTML never uses counts a quarter towards the
  brand's colours and fonts, so a site's own buttons lead its palette
  rather than years of components it no longer shows.
…s seen bears it out

A measurement can land on the wrong element: on wise.com the reader took
an old blue button its bundle still styles for the main one, and the
faithful look's brand colour, band and headline went blue and slate over
a model that had seen the lime and forest green in the screenshot. A
measured colour now counts only when it is among the screenshot's colours,
or without a screenshot among the site's leading ones; a label's black or
white needs no evidence. Exact sizes, weights and corners still apply.
…the faithful look takes it

A screenshot's hero decides a look more than anything else on it, and a
focused question answers more surely than the same question folded into a
whole look. Alongside the looks, a short vision call reads only the hero:
its fill and colour, a panel of a second colour within it, an inset card,
where the words sit, whether the header shares its colour, art along its
foot, and capitals. The faithful look takes it last, as what was seen;
its colours count only where the screenshot's own colours bear them out.

The site's hero colour no longer gives way to the model's second colour
when both are set.
…rce confirms a colour

From the round with the hero read:

- A hero that is the plain page no longer clears the band: Stripe's
  white hero carries its colour in a gradient beside the words, which the
  look had given the band. The hero says what the band is, and whether the
  header shares it, only when it has a fill of its own.
- A measured colour is confirmed by the screenshot's colours or the site's
  leading ones. A brand colour used only on small buttons is hardly in a
  screenshot by area, so Stripe's blurple was refused and its look went
  orange; an old colour a bundle still styles is in neither.
…s a look's gaps once

One look to an answer keeps the strict grammar a third of three looks'
size, so a smaller model drops fewer fields, and the faithful look is not
pulled toward the other two; the three are asked for together. Each look
is linted for what a model can leave out: empty lines, a page titled only
with its name, a feedback headline repeating Home's, a band in a second
colour with none given, an accent phrase not in a headline, fewer than
three Home blocks. A look with gaps is asked to mend only those, and the
mended one is kept only when it has fewer. A look whose call fails leaves
its place empty, so the faithful look is never another in its place.
…ed extractor does

The reader takes the principles that make a rendered extractor read a
brand's roles well, as far as a page's HTML and CSS allow:

- The main button is the call to action the page leads with, ranked by
  its words (get started, sign up, send money), its place near the top
  and its styling, past sign-in links and consent notices, among those
  whose fill can be read.
- Colours a site keeps under a theme class on <html> or <body> resolve,
  as well as its root's.
- The main action's colour leads the palette when the CSS corroborates
  it, as one of the site's leading colours or its buttons'. A page themed
  as it runs leaves only its stylesheet's default theme, which no reader
  of the CSS alone can tell from the brand, so that stays as it was.
… element tree

The reader recovers more of what a page shows without running it:

- The page is read as a tree, so custom properties inherit down it as a
  browser's do. Sites keep a theme's colours on a wrapper as often as on
  the root, and palette classes on an ancestor choose a token's value.
- Colour, weight, tracking and case inherit from above where an element
  sets none.
- Inline styles apply over the stylesheet.
- Anything inside a hidden element or a consent notice is passed over.
- Viewport-height units resolve at a standard screen.
- Repaints for a single browser engine or a wide-gamut screen are left
  out, as a wide, light screen never applies them.
- Sites that split their styles into many sheets are read whole, up to
  sixteen sheets within a budget.

The cascade moves to its own module and files rules by the class, id
or tag their last compound needs, so matching an element looks only at
rules that could apply.
…tor sees it

The headline is the largest h1 in sight unless display text near the
top of the page is much larger. A site may mark a small label up as an
h1, or keep its h1 for screen readers and show the hero in another
element. Any visible element near the top that holds words of its own
can be that text, whatever its tag or classes.
…ing order

Display text counts as near the top by its place among the page's first
worded elements, not by how far into the markup it sits: long attributes
and menus can fill much of a page's markup before what it shows first.
Comment on lines +52 to +56
const text = body
.slice(starts.get(index)!, at)
.slice(0, 600)
.replace(/<[^>]+>/g, ' ')
.replace(/&amp;/g, '&')

This branch has not been deployed

No deployments
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.

2 participants