Skip to content

feat: Branding module — customize app appearance#273

Merged
antosubash merged 14 commits into
mainfrom
worktree-branding-module
Jun 26, 2026
Merged

feat: Branding module — customize app appearance#273
antosubash merged 14 commits into
mainfrom
worktree-branding-module

Conversation

@antosubash

@antosubash antosubash commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a Branding module that lets an admin customize a SimpleModule app's appearance from one page (/branding/manage, gated by a new Branding.Manage permission):

  • Identity — application name, logo, and favicon (uploaded via the FileStorage module).
  • Colors — primary color (light + dark); the full palette (--color-primary-hover/-light/-subtle/-ring) is derived from the chosen color via color-mix, so hovers and focus rings follow the brand.
  • Top bar — a configurable, optionally-dismissible announcement bar.
  • Footer — configurable text, copyright, and links.
  • Advanced — a custom-CSS box.

The admin page includes a live preview.

Screenshots

Branding applied across the app — custom top bar (message + link + dismiss), brand mark and app name, primary color on navigation/headings/buttons, and the custom footer:

Branding applied

Admin configuration page — Identity, Colors, Top bar, Footer, and Custom CSS, with a live preview panel:

Admin manage page

How it works

  • Storage: branding values are persisted via the existing Settings module (ISettingsContracts) by key — no new DbContext. They are intentionally not registered as SettingDefinitions, so they don't appear in the generic Settings admin page (they're edited via the dedicated Branding page); defaults live in BrandingDefaults/BrandingService.
  • Runtime application, two channels:
    1. A module middleware publishes a branding Inertia shared prop → React chrome (app name, logo, top bar, footer) renders it on every page.
    2. A small, generic framework addition — IInertiaHeadContributor + a <!--HEAD_CONTRIBUTIONS--> placeholder in index.html, resolved per-request by the renderer (fail-open, skipped when the placeholder is absent) — injects the color overrides, custom CSS, and favicon into <head> server-side so colors apply with no flash. Reusable by any future module.
  • Assets: logo/favicon are served by an anonymous GET /api/branding/assets/{kind} endpoint (FileStorage's own download is permissioned, so it can't serve a logo on the login page). Uploads and serving are restricted to a raster/icon allowlist (no SVG — it's served anonymously and could carry script), with X-Content-Type-Options: nosniff. Top-bar/footer link URLs are sanitized against javascript:/data:/vbscript:.

Verification

  • Browser-verified at /branding/manage (admin): edit name/logo/colors/top-bar/footer → save → applies app-wide (sidebar, top bar, footer, primary color + derived palette, document title) with no flash; persists across restart; anonymous asset endpoint returns 404 when unset.
  • New e2e spec tests/e2e/tests/flows/branding-crud.spec.ts (API round-trip + head-injection + admin-page render + asset 404).
  • Local CI green: biome check · validate-pages/i18n/framework-scope · typecheck (15/15) · full dotnet test · dotnet build -warnaserror · npm run build · e2e smoke (66/66).

Test plan

  • Reviewer opens /branding/manage as an admin, changes the app name + primary color + enables the top bar/footer, saves, and confirms the chrome updates across the app.
  • CI is green.

…mary palette (hover/light/subtle/ring) from custom color
…itions

They are edited via /branding/manage, not the generic Settings admin page; registering them polluted that page (two extra 'Primary color' fields) and broke Settings color smoke tests. Values are still stored/read by key via ISettingsContracts (no definition required); defaults live in BrandingDefaults/BrandingService. Adds a branding e2e spec.
…layers

The CI docker job failed restoring SimpleModule.Host.csproj because the
Branding module's project files were never added to the Dockerfile's
per-project COPY list (used for restore-layer caching). Add the Contracts
and implementation .csproj to both Dockerfile and Dockerfile.worker
(kept aligned per the worker file's convention) and the frontend
workspace package.json to the Host npm-restore stage.
…ward

actions/setup-dotnet@v5 rejects a non-full SDK version ("10.0.0") when
rollForward is specified, breaking the GitHub-managed Automatic Dependency
Submission (NuGet) workflow (which runs setup-dotnet with no explicit
dotnet-version and so reads global.json). Use the full feature-band
version "10.0.100"; rollForward: latestMajor still selects the latest
installed 10.x SDK.
@antosubash antosubash merged commit 124bd2a into main Jun 26, 2026
10 checks passed
@antosubash antosubash deleted the worktree-branding-module branch June 26, 2026 15:00
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.

1 participant