Skip to content

UI branding#193

Merged
MiloCasagrande merged 5 commits into
mainfrom
feat/ui-branding
Jul 3, 2026
Merged

UI branding#193
MiloCasagrande merged 5 commits into
mainfrom
feat/ui-branding

Conversation

@MiloCasagrande

@MiloCasagrande MiloCasagrande commented Jul 1, 2026

Copy link
Copy Markdown
Member

This is the work to brand the UI via a single branding.json file. There is a branding.md document to describe how to use the feature.

Dark mode works only if the browser/system is set into dark mode.

There are 4 commits, split into the different parts of the work (general branding, favicon, dark mode, branding doc), I can squash them and keep only 2 in case.

A couple of screenshots to visually see it:

ui-branded-light ui-branded-dark

Milo Casagrande and others added 4 commits July 1, 2026 11:43
Branding is read once at startup from <data-dir>/branding/branding.json;
when absent uses the built-in defaults.

- Add a CSS custom-property token layer in style.css (brand/accent/surface/
  text) bridged into PicoCSS variables; replace hardcoded colors with tokens.
- Use app title, brand text, and an optional logo from branding.
- Hash the rendered stylesheet for the ETag so a rebrand busts stale caches.

PicoCSS is retained as the renderer; the token layer is framework-agnostic
to support a later redesign.

Signed-off-by: Milo Casagrande <mcasagra@qti.qualcomm.com>
Ship a default favicon (previously the app had none) and let operators
override it through the branding system.

- Add a validated "favicon" field to branding.json (accepts svg/ico/png).
- Add a <link rel="icon"> to the page head; the format is driven by the
  response Content-Type.

Signed-off-by: Milo Casagrande <mcasagra@qti.qualcomm.com>
Dark mode previously ignored operator branding and used fixed colors.
Tokenize the dark palette so it is as customizable as light.

- Add an optional "colorsDark" object to branding.json.
- Tokenize the dark @media block to consume the dark tokens instead of
  hardcoded values.

Signed-off-by: Milo Casagrande <mcasagra@qti.qualcomm.com>
Signed-off-by: Milo Casagrande <mcasagra@qti.qualcomm.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread server/ui/web/handlers.go Dismissed
@MiloCasagrande MiloCasagrande requested a review from doanac July 1, 2026 14:35

@doanac doanac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool. I have one nit - but maybe I'm wrong in my opinion.

Comment thread server/ui/web/branding.go
SurfaceAlt *string `json:"surface-alt"`
Text *string `json:"text"`
} `json:"colors"`
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I get the value of having two different structs? I think I'd stick with just the brandingFile idea but get rid of the pointers do length == 0 checks instead checks for nil.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pointers are overkill, yeah. I'll switch to plain strings and drop the nil checks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the two structs, you mean brandingFile and Branding?

In that case I think it's still worth keeping them both, as they serve different purposes.

brandingFile mirrors the raw JSON of the branding.json file used to customize the UI colors.

Branding is the flat version, template-friendly with defaults applied: this is guaranteed to be populated and valid via LoadBranding.

Staying only with brandingFile would lead to move the "default value resolution/validation" logic into the templates, and we might have to duplicate it in multiple places.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking more of having something like an "applyDefaults" method added to the brandingFile struct. Then pass that to the templates and everything would be in place for it?

If that approach looks bad - stick with this. I'm fine either way. Merge at your leisure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reasons I still prefer the two structs approach is that the only way to get the branding is via calling that LoadBranding func. With applyDefaults and a single struct there would be less code, but there's nothing stopping a caller to forget to call it, handing down to a template a struct with non-default values.

I'll keep it as is for now, but we can rework it later if it feels too much.

Signed-off-by: Milo Casagrande <mcasagra@qti.qualcomm.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@MiloCasagrande MiloCasagrande merged commit cf36cbb into main Jul 3, 2026
14 checks passed
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.

3 participants