feat(branding): optional per-app banner replacing logo + name in header

A brand may ship a wide banner (logo + wordmark in one image) that
replaces the brand-kit logo + app-name pair in a standalone's header.
Events is the first consumer.

Banners are optional and resolve at build time, mirroring the existing
@brand-app-logo chain:

- resolveAppBanner(app?) checks per-standalone override first
  (branding/<dep>/icons/<app>/banner.{svg,png}) then the brand's primary
  banner (branding/<dep>/banner.{svg,png}); returns null when absent
  instead of throwing, so brands without a banner keep logo + name.
- brandAppBannerAliasEntry() always registers the @brand-app-banner
  alias (falling back to the logo) so the static import resolves; whether
  it renders is gated by the VITE_APP_BANNER build flag.
- EventsPage renders the banner when the flag is set, else logo + name.

Deployers override per-standalone without touching the component. SVG
banners must have their text outlined to paths (browsers lack designer
fonts) — documented in branding/README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-15 22:11:01 +02:00
commit 8f2c401e00
5 changed files with 119 additions and 7 deletions

View file

@ -8,9 +8,11 @@ import { visualizer } from 'rollup-plugin-visualizer'
import {
brand,
brandAlias,
brandAppBannerAliasEntry,
brandAppLogoAliasEntry,
brandAssetsPlugin,
brandManifestName,
resolveAppBanner,
} from './vite-branding'
/**
@ -55,6 +57,12 @@ function eventsHtmlPlugin(): Plugin {
const APP_NAME = brandManifestName()
process.env.VITE_APP_NAME = APP_NAME
// When the active brand ships a banner (wide logo+wordmark lockup), the
// events header renders it in place of the logo + name pair. Surfaced as
// a '1'/'' flag the component reads; the actual file comes through the
// @brand-app-banner alias below. See branding/README.md.
process.env.VITE_APP_BANNER = resolveAppBanner('events') ? '1' : ''
export default defineConfig(({ mode }) => ({
base: process.env.VITE_BASE_PATH || '/',
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
@ -128,6 +136,7 @@ export default defineConfig(({ mode }) => ({
// aliases without one shadowing the other.
alias: [
brandAppLogoAliasEntry('events'),
brandAppBannerAliasEntry('events'),
...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })),
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
],