fix(frontend): derive primary/ring shades so branding overrides propagate - #169
Conversation
…gate
`frontend.branding.theme.{light,dark}` lets a deployer override --primary,
--primary-foreground and --ring at runtime via /config.json, but index.css
also hardcoded four tokens to the Nebari magenta with no path back to
--primary: --primary-hover, --sidebar-primary, --sidebar-primary-foreground
and --sidebar-ring.
--primary-hover backs `hover:bg-primary-hover` / `active:bg-primary-hover` on
Button and Badge, so a deployment rebranded to a non-purple primary renders
correctly at rest and then flashes Nebari magenta on every button hover.
Observed on a live cluster.
Derive the four from the tokens branding can actually reach, mirroring what
nebari-landing already does for --sidebar-primary. --primary-hover is not a
plain alias (darker in light mode, lighter in dark) so it becomes a
color-mix() against black/white; the mix percentages were chosen to reproduce
the previous literals. oklab rather than oklch is used for the mix so the
result never depends on how an achromatic anchor's powerless hue is handled.
Also make the documented contract honest: add primaryHover, sidebarPrimary,
sidebarPrimaryForeground and sidebarRing to ThemeTokens, values.yaml and the
Configuration docs, and note that ThemeTokens is a compile-time type rather
than a runtime allow-list - toCssVars() applies whatever keys it is handed, so
unlisted keys currently pass through as --kebab-case custom properties.
Behaviour is unchanged there; it is now documented as unsupported instead of
silently relied on.
Adds a test over index.css so re-pulling the theme (`shadcn add @nebari/theme`)
cannot quietly restore the literals.
|
📄 Docs preview for |
Aligns with nebari-dev/provenance-collector-pack#81, which lands the same derivation against a byte-identical index.css. 18% is also the ΔE-optimal point against the previous literal oklch(69.98% 0.1926 311.48): 0.019 oklab ΔE at 18% vs 0.020 at 20%. The two packs should not diverge on this constant.
|
Pushed 5bce9d3: the dark 18% is also the better constant on its own merits. Sweeping N against the previous literal
18% is the minimum. I had rounded to 20% for tidiness, which traded away accuracy for nothing — 21% is the point that matches the old lightness exactly, and 18% is the point that minimises total error, so 20% was neither. The PR body table above is updated to match. Gates re-run on the new commit: |
* fix(ui): derive primary/ring shades so branding overrides propagate --primary-hover, --sidebar-primary, --sidebar-primary-foreground and --sidebar-ring were pinned to the magenta primitives, which a runtime ui.branding.theme override of `primary` / `primaryForeground` / `ring` cannot reach. A deployment rebranded to a non-magenta primary therefore renders correctly at rest and flashes Nebari magenta on every button, badge, switch, slider, checkbox and radio hover or press. The three sidebar tokens were byte-identical to --primary / --primary-foreground / --ring, so they become plain aliases. --primary-hover is a darker shade in light mode and a lighter one in dark, so it becomes a color-mix against black/white, in oklab (a rectangular space, so mixing toward an achromatic anchor cannot drift the hue). 15% / 18% reproduce the previous literals to 0.003 / 0.019 oklab dE. primaryHover, sidebarPrimary, sidebarPrimaryForeground and sidebarRing are added to ThemeTokens, values.yaml and the Branding docs, which also now record that the token list is the supported contract rather than a runtime filter - toCssVars() applies any key it is given. Matches nebari-dev/llm-serving-pack#169, nebari-dev/provenance-collector-pack#81 and nebari-dev/nebari-landing#200. * docs: tidy the derived-token wording
The defect
frontend.branding.theme.{light,dark}(chart →/config.json→applyAppConfig()infrontend/src/app/config.ts) lets a deployer override a documented set of theme tokens at runtime, includingprimary,primaryForegroundandring.frontend/src/index.cssalso hardcoded four tokens to the Nebari magenta (oklch hue ~311), none of which were reachable fromprimary/ring:--primary-hoverhover:bg-primary-hover/active:bg-primary-hoverincomponents/ui/button.tsxandcomponents/ui/badge.tsx--sidebar-primary--color-sidebar-primary--sidebar-primary-foreground--color-sidebar-primary-foreground--sidebar-ring--color-sidebar-ringUser-visible symptom
A deployment rebranded to a non-purple
primaryrenders correctly at rest and then flashes the old Nebari magenta on every button and badge hover/press. Confirmed on a live cluster. The sidebar tokens are the same defect but currently latent — nothing in this app renders a sidebar yet.The fix
1. Derive instead of hardcode (
frontend/src/index.css), mirroring what the siblingnebari-landingalready does for--sidebar-primary:--primary-hovercan't be a plain alias — it is a darker shade in light mode and a lighter one in dark — so it becomes a mix against black/white, with percentages chosen to reproduce the previous literals:in oklabrather thanin oklch: black and white are achromatic, so their hue is powerless, and mixing in a rectangular space means the result never depends on how that is handled. For these anchors the two spaces are mathematically identical anyway when powerless hue is carried forward.2. Make the contract honest.
primaryHover,sidebarPrimary,sidebarPrimaryForegroundandsidebarRingare added to theThemeTokenstype, thevalues.yamlcomment and the Configuration docs, so an explicit override of a derived shade stays supported and documented.The docs and
values.yamlalso now state that the token list is the supported contract, not a runtime filter:ThemeTokensis a compile-time type,toCssVars()iteratesObject.entries(tokens)with no allow-list, and the chart passes the map throughtoJsonwith novalues.schema.json. Any camelCase key therefore lands as its--kebab-casecustom property today. Behaviour is unchanged — filtering would break deployers who are already relying on that — but it is now written down as unsupported rather than silently depended on.3. Regression guard.
frontend/src/index.css.test.tsasserts the four tokens referencevar(--primary)/var(--primary-foreground)/var(--ring)in both:rootand.dark. The file header tells maintainers to re-pull the theme withshadcn add @nebari/theme, which would restore the literals; this makes that visible in CI. Verified the test fails when a literal is put back.No version bump — per the CI/Releasing docs, bumping
Chart.yamlversionis what cuts a release.Verification
Default appearance does not regress. Rendered sRGB of the new derivations against the old literals:
--primary-hoveroklch(47.01% 0.1577 311.26)→#77399aoklch(47.02% 0.1603 311.45)→#77379a--primary-hoveroklch(69.98% 0.1926 311.48)→#c575f4oklch(68.82% 0.1770 311.67)→#bf76e9--sidebar-primary,--sidebar-primary-foreground,--sidebar-ring--primary/--primary-foreground/--ringLight is visually identical. Dark is a hair darker and less saturated — 18% is the ΔE-optimal point, and the closest a mix toward white can get, since white contributes no chroma. On a transient hover fill this reads the same; happy to pin the dark literal instead if a maintainer wants it byte-exact.
Build output. Lightning CSS (via Tailwind v4) emits a graceful fallback on its own, so no
@supportsguard is needed here:i.e. browsers without
color-mix()fall back to the un-shaded primary rather than to an invalid value.Commands run (all from a clean clone, all pass):
frontend/npm cifrontend/npm run build(tsc -b && vite build)frontend/npm testfrontend/npm run check(Biome, thelint-frontendCI job)biome.jsonschema versionhelm lint charts/nebari-llm-serving/helm template … --set-json 'frontend.branding.theme=…'/config.jsonrendersprimaryHoverthrough correctlydocs/npm ci && npm testdocs/npm run buildSKIP_BUILD=1 bash scripts/check-links.shLINKS_OKGo tests for
operator/andkey-manager/were not run — no Go files touched.Related
nebari-dev/provenance-collector-packhas the identical defect — the twoindex.cssfiles appear to share an origin — and is getting a parallel PR.