The tenant admin Branding page (in the LMS admin menu) never had a color editor
in any committed version. Colors could only be set from the platform-admin tenant
Branding tab, using the keys primary, secondary, and
accent. A tenant admin had no way to change their own theme colors.
The public site footer used the tenant secondary color as its background. When a tenant logo was drawn in that same color, the logo blended into the footer and became hard to see.
Three options were considered for the footer color:
We chose (c). It has the lowest friction, it never overwrites what the user typed, and complementary hues often clash. The footer color falls out of the primary color by default, and a tenant can still set an exact footer color when they want one.
Frontend only. 10 files, about 270 lines.
BrandingManagement.tsx: a new Colors card with
primary, secondary, and an optional footer field. Each field has a native color
swatch, a hex text input, and a Clear button. Hex is validated as
#rrggbb, lowercased on save. Other color keys (such as
accent) are preserved, unset keys are removed, and the card is wired
into the page's dirty and discard state.
theme.ts: footer added to TenantBrandColors.
New buildFooterVariables: an explicit valid footer hex is used as-is;
otherwise the footer is derived from the primary hue at lightness
0.22, with chroma scaled to 0.35× the primary and
capped at 0.1. It sets --footer and
--footer-foreground, and both are cleared by
clearTenantTheme.
index.css: new --footer / --footer-foreground
tokens. The default equals the old --secondary, so the default
SkillPixel tenant looks the same as before.
Footer.tsx: background changed from bg-secondary to
bg-footer. Custom logos are now wrapped in a white rounded plate so a
dark logo cannot melt into the footer.
en/vi admin.json translations added, plus regenerated resource types.
theme.test.ts gained 4 new test cases.
footer added to BRANDING_COLOR_KEYS, plus a hint line
under the Colors section explaining the footer is optional and derived from the
primary color when empty.
Ran the full stack with Docker Compose from the worktree, seeded the database, and logged in headless as an admin using a minted Firebase token driven with Playwright.
| Check | Result |
|---|---|
| Colors card renders on desktop | Pass |
Renders at 360px, scrollWidth 360 (no horizontal scroll) | Pass |
Save persists primary #c21230 / secondary #22305f to DB and bootstrap | Pass |
Footer with no override = oklch(0.22 0.0705 21.43) (dark red tint) | Pass |
Footer override #101820 = oklch(0.205 0.02 248.83) | Pass |
Clearing restores oklch(0.4084 0.1234 257.3) default | Pass |
| Invalid hex "zzz" shows "Enter a 6-digit hex color like #1a2b3c." and blocks save | Pass |
| No console errors | Pass |
#101820.sessionStorage, so a saved
color only shows in a fresh tab. The backend bootstrap cache hint says about
5 minutes for the public site.
Host header
localhost:3000.