UI branding#193
Conversation
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>
doanac
left a comment
There was a problem hiding this comment.
This is cool. I have one nit - but maybe I'm wrong in my opinion.
| SurfaceAlt *string `json:"surface-alt"` | ||
| Text *string `json:"text"` | ||
| } `json:"colors"` | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
The pointers are overkill, yeah. I'll switch to plain strings and drop the nil checks.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
This is the work to brand the UI via a single
branding.jsonfile. There is abranding.mddocument 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: