Dashboard Branding white-labels the admin chrome — the dashboard sidebar and the login / auth pages — so an install can wear the platform's brand, the client's site brand, or a web designer's agency brand. It controls both the logo and the chrome colors (sidebar accents, tabs, pills, links, brand buttons), gated behind a paid-member feature toggle.
The three modes
A single radio control on Dashboard → Settings → White Label → Dashboard & Login Branding picks one of three modes (stored as branding.admin_logo_mode). Each mode drives the logo and the chrome colors together — they never diverge.
Mode (branding.admin_logo_mode) |
Logo | Chrome colors |
|---|---|---|
webprocms |
The shipped platform logo | Fixed WebProCMS purple (#495092) + teal accent (#0d9488). Identical in light and dark. |
site |
The client's site logo (the one set on the Branding page) | Auto-derived from the site's primary and secondary brand colors. No extra configuration. |
designer |
The agency's own logo (light + dark uploads) | The agency's own four swatches: primary light/dark and accent light/dark. |
The logo and colors are deliberately one decision: choosing "this site's brand" means the dashboard adopts the client's logo and their colors; choosing the designer brand adopts the agency's logo and their swatches. There is no way to mix (e.g. client logo + agency colors) — that's by design, so the admin always reads as one coherent brand.
Who can do what
- The mode radio is available to any member (it appears once
Features::enabled('dashboard_branding')is true). designermode can only be selected by a non-super once a designer logo already exists; a Super can select it first to reveal the upload UI.- The designer logo uploads and the four color swatches are Super-only — they live inside the same box and only render in
designermode for a Super. This keeps the agency's own brand assets out of reach of the client's day-to-day admins.
Feature gating — paid members only
Dashboard Branding is a registered feature (dashboard_branding in Features::all(), default: true, available: true). It is default-on for paying members and hidden for everyone else:
- The entire box and its pill-nav entry render only when
Features::enabled('dashboard_branding')is true. AdminLogo::mode()resolves towebprocmswhenever the feature is disabled, so a non-member install always shows the platform logo and colors regardless of any stored mode.- Membership enforcement is layered on top via the Memberships feature gate (Memberships → Settings → Licensing → Require membership for features, registered by
MembershipsServiceProvider), which vetoes every Features entry for non-members. On an install that opts into that gate, toggling membership off instantly reverts the chrome to WebProCMS.
The color model
Chrome colors are emitted by BrandingStyleService::buildDashboard() as CSS variables in the dashboard's inline <style> block. Two token families carry the chrome brand:
--color-cms-primary(+ 19 shades) — the dashboard's brand color: links, active tabs, pills, the sidebar active state, brand buttons, and Flux's--color-accentalias.--color-cms-accent(+ 19 shades) — the accent role: stats numbers, pricing checkmarks, feature icons, charts. Never buttons or links.
Each family is resolved per mode by dashboardColorScheme(), which returns explicit light + dark bases. Light bases are written into :root; dark bases into a trailing .dark { } block (the dashboard toggles .dark on <html>).
Why solid fills keep their base in both themes
generateShadeOklch() normalizes lightness per shade — --color-cms-primary-300 is always a light shade regardless of the base. So:
- Solid fills (
bg-cms-primarybuttons, active pills) keep the base color in both light and dark mode — a colored button looks the same in both themes, which is correct. Indesignermode the dark swatch can shift hue/chroma, but it is never inverted to a light color. - Inline links and active-tab text in dark mode use the lightness-normalized shade (
dark:text-primary-300/dark:text-cms-primary-300) so they stay legible on dark backgrounds.
Auto-contrast button text
Brand buttons reference --color-cms-on-primary for their label color through the text-on-cms-primary utility. onPrimaryFor() computes it from the primary base's OKLCH lightness: a base at lightness ≥ 0.62 (a light/yellow primary) gets dark text; anything darker gets light text. This is computed separately for the light and dark primary bases, so an agency only ever picks a button background — the label contrast is automatic and stays readable on any picked color.
Designer mode color swatches
In designer mode a Super sees four color pickers, each an optional 6-digit hex stored as a Setting:
| Swatch | Setting | Fallback when blank |
|---|---|---|
| Primary (light mode) | agency.cms_primary_light |
WebProCMS purple |
| Primary (dark mode) | agency.cms_primary_dark |
the light primary, then WebProCMS purple |
| Accent (light mode) | agency.cms_accent_light |
WebProCMS teal |
| Accent (dark mode) | agency.cms_accent_dark |
the light accent, then WebProCMS teal |
Leaving a dark swatch blank reuses its light sibling — the common case where an agency only has one brand color and wants it in both themes. Saving the swatches (or switching modes) busts the BrandingStyleService cache so the new chrome appears on the next dashboard load.
Transactional email chrome
Outgoing feature emails (order notices, booking confirmations, membership dunning, activity alerts, review requests, ticket replies, affiliate invites, social-posting notices) all render a shared header. By default it's the site name as text; the branding box's Transactional Email Logo picker swaps in a logo image (capped at 40px tall) across every one of them at once. The shared renderer is EmailChrome::headerHtml() — feature-gated, so turning Dashboard Branding off reverts every email header to the site-name text. The from name was already white-label: it resolves Marketing's from-name setting, then the business name — never "WebProCMS".
(The shop's order emails keep their own richer logo support via branding.logo_url, and Client Billing keeps its business-name header — invoices identify the billing entity.)
Help & support links
The branding box's Help & Support Links repeater (label + URL rows) renders custom links at the bottom of the dashboard sidebar for every user — point clients at your agency's support desk, docs, or contact page instead of leaving them to hunt. Feature-gated like everything else in the box; blank rows are dropped on save.
Agency credit in the footer
The Web Designer / Agency Credit box (Settings → White Label, Super only) sets the "Designed by" line every footer row can show. It is the one place the credit is configured, separate from the dashboard branding mode and not feature-gated.
| Field | Setting | What it does |
|---|---|---|
| Agency or designer name | agency.name |
The name in the credit. Leaving it blank hides the credit on every footer. |
| Agency or designer website | agency.url |
Where the name links, or the keyword when it has no page of its own. |
| Keyword link text | agency.keyword |
Optional. Makes the credit read "Designed by WebProJoe, Affordable Web Design", with only the keyword linked and the name as plain text. |
| Keyword link URL | agency.keyword_url |
Optional. The page the keyword links to, typically an inner page such as the agency's web design services page. Falls back to the website. |
AgencyCredit::link() renders the credit. Footer rows print their own label (Designed by / Design by:) and then call it, so changing the wording or the link never means rewriting a footer blade. Every part is escaped. Every URL goes through link_url(), which localizes a link to the site's own host for /es/ visitors and turns an unsafe scheme into #. A keyword with no URL anywhere renders as plain text. Saving the box clears the response cache when a value changed, because the credit appears in the footer of every cached page.
Each footer's quick settings (Templates → footer) keep the Show "Designed by …" toggle, and its label now shows the full credit text.
Footers installed before the keyword existed. Footer partials are materialized per install, so the old inline anchor (@if ($agencyUrl)<a href="{{ $agencyUrl }}" …>{{ $agencyName }}</a>@else…@endif) stays in them after an update. AgencyCreditHealer rewrites that anchor into the AgencyCredit::link() call, keeping its link classes. With no keyword set, a healed footer shows the same credit as before (a footer older than link_url() also gains its URL handling). It scans the layout partials and the install's custom rows. A credit hand-edited into another shape no longer matches and is left alone. Each file is rewritten under its editor save lock, and a file that is mid-save or unreadable is left for the next run. The release's migration only schedules the heal. The 15-minute pages:reconcile-index sweep runs it after the update, retries a failed run up to three times, and then logs the manual command: php artisan design:heal-agency-credit.
What lives where
| Path | Purpose |
|---|---|
app/Support/AdminLogo.php |
Resolves the effective mode and the light / dark / login logo URLs. Gates on Features::enabled('dashboard_branding'). |
app/Services/BrandingStyleService.php |
dashboardColorScheme() (per-mode light/dark bases), onPrimaryFor() (auto-contrast), buildDashboard() (emits :root + .dark chrome tokens). |
resources/views/pages/dashboard/settings/⚡general.blade.php |
The "Dashboard & Login Branding" box — mode radio, designer logo uploads, four color swatches, email-logo picker, help-links repeater, and the save handlers. |
app/Support/EmailChrome.php |
Shared transactional-email header (logo or site name) used by every feature module's email wrapper. |
app/Support/AgencyCredit.php, app/Support/AgencyCreditHealer.php |
The footer "Designed by" credit renderer, and the heal that moves materialized footers onto it. Tested by tests/Feature/AgencyCreditTest.php. |
resources/css/app.css |
@theme declarations for --color-cms-accent (+ shades) and --color-cms-on-primary, plus the text-on-cms-primary utility. |
resources/views/components/app-logo.blade.php and the three auth layouts |
Render the resolved logo on the sidebar and login pages. |
tests/Feature/AdminLogoTest.php, tests/Feature/BrandingStyleServiceTest.php |
Mode resolution, feature gating, per-mode light/dark emission, auto-contrast, swatch persistence. |
Marketing angle
Run a fleet of client sites — or sell WebProCMS itself — and the admin shouldn't shout someone else's brand. Dashboard Branding lets a paid member white-label the entire dashboard and login experience in one decision: show the client's own logo and colors, or your agency's. Pick a mode and the sidebar, tabs, links, and buttons all retune to match — light and dark mode handled, button-text contrast set automatically, no CSS to write. Hand a client a dashboard that looks like their product, or stamp every build you ship with your agency's mark.
Notes
- The feature is one toggle on the Features page; flipping it off reverts the chrome to WebProCMS instantly (logo and colors) without deleting any stored mode or swatches.
sitemode follows the live site brand — change the primary/secondary on the Colors page and the dashboard chrome follows on the next load. No duplicate color entry.--color-cms-accentis provided as a token for stats/charts; it is intentionally wired to neither buttons nor links. Neutral (zinc/Flux) buttons are never brand-colored — only elements already styled withcms-primaryretune per mode.- The legacy single "Dashboard Primary Color" override was removed when colors became mode-driven;
sitemode subsumes it (the dashboard follows the site primary) anddesignermode supersedes it with explicit light/dark swatches.