Skip to content

feat(branding): end-to-end branding — header, footer, colour ramp, title & emails - #222

Merged
antosubash merged 4 commits into
mainfrom
worktree-branding-header-footer
Jun 26, 2026
Merged

feat(branding): end-to-end branding — header, footer, colour ramp, title & emails#222
antosubash merged 4 commits into
mainfrom
worktree-branding-header-footer

Conversation

@antosubash

@antosubashantosubash commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add an app-shell footer to the authenticated + admin layouts (SidebarLayout) — previously only the public landing page had a footer. <main> is now a flex column so the footer sticks to the bottom.
  • Make branding-driven, not hardcoded: the public footer and the auth-card (AuthCardShell) brand lockup now read the branding shared prop (appName/logoUrl) instead of literal simple_module/python text — correct for white-labelled deploys.
  • Centralize brand metadata in packages/ui/src/lib/brand.ts (BRAND_ACCENT gradient, BRAND_DEFAULT_APP_NAME, repo URL, license, footer links), killing 4× duplication and unifying the unbranded fallback name.
  • BrandingMark gains an optional stacked caption, a size="lg" variant, and a badgeClassName shadow override — it's now the single brand-lockup primitive used by the header, footer, and auth shell.
  • New reusable BrandingFooter component (app/public variants) with unit tests.

Verification

  • Code review (high effort, 2 passes): 4 findings found + fixed (load-bearing one: a justify-between flex bug that detached the footer badge from its wordmark); final pass clean.
  • QA (browser, normal depth + responsive): 0 bugs across 4 surfaces — public landing, auth login, dashboard, users-admin — at desktop and mobile (375px). Footer badge attached to wordmark, © <year> · MIT caption, Docs/Changelog/GitHub links resolve, no horizontal overflow, nav collapses on mobile. Evidence in .qa/screenshots/.
  • Local CI:make lint ✓ · make test ✓ (1405 Python + 26 JS tests pass; e2e excluded as in pr.yml).

Round 2 — end-to-end theming (commit 482784c)

The lockup work above brands what's visible; this round makes a configured deployment branded everywhere a tenant value should reach:

  • Custom primary colour recolours the whole palette, not just buttons. New packages/ui/src/lib/color.ts derives the full --color-primary-50…900 ramp from the single brand hex (hex→OKLCH, re-hue + chroma-scale, preserving the designed lightness ladder); BrandingHead writes it inline on :root. The compiled CSS confirms .from-primary-600{--tw-gradient-from:var(--color-primary-600)}, so the logo-badge gradient and auth mesh blobs now follow the tenant colour (previously they stayed emerald while only bg-primary buttons changed). Near-grey picks → grey ramp; vivid picks stay vivid.
  • Branded document <title> + pre-hydration head.lib/app-title.ts + app.tsx seed the app name into Inertia's title callback (tabs read Dashboard — Acme), and a duck-typed branding_head() Jinja global server-renders the branded <title>, favicon <link>, and <meta name="theme-color">before React hydrates (no default-brand flash; correct for crawlers). No framework→plugin import (SM009 clean).
  • Transactional emails carry the brand. Invite / verify / reset subjects + bodies now include the app name, threaded through a decoupled app_name provider — users never imports branding (reads app.state by name).

Round 2 verification (/ship)

  • Code review (high, 2 passes): pass 1 found 5 findings — all fixed in ead121d: (1) a CR/LF in app_name could break email Subject headers → now rejected at the source via a shared clean_app_name; (2) dropped a framework→file_storage route coupling (server favicon link) in favour of the existing client-side favicon; (3) moved a side-effect out of a React render body + made theme-color reactive; (4) derived a duplicated chroma constant; (5) accepted the framework default-name literal as required by the SM009/usersbranding decoupling. Pass 2 re-review: clean.
  • Browser QA (/qa, normal depth): 0 bugs. Verified live that setting a magenta #c026d3 re-hues the full ramp — sidebar badge gradient→ oklch(… 322.9°), auth-screen mesh blobs, --primary, and <meta theme-color> all follow it; tab title → the app name; revert-to-default cleanly restores emerald. Evidence in .qa/reports/qa-report-branding-round2-iteration-1.md.
  • New tests: colour util (color.test.ts), title util (app-title.test.ts), branding_head (test_branding_head.py), rendered shell title + theme-color hot-swap + control-char rejection (test_branding.py), email branding (test_mailer.py).
  • Local CI:make lint ✓ (exit 0) · make test ✓ (1413 Python + 41 JS tests pass).

Test plan

  • Load / and /dashboard/ — confirm the footer renders the brand lockup + Docs/Changelog/GitHub links
  • Confirm the /auth/login card shows the prominent glowing brand badge
  • In Branding admin, set a non-emerald primary colour — confirm the sidebar/auth badge gradient and login mesh blobs recolour, and the browser tab title shows the app name
  • CI is green

https://claude.ai/code/session_01GYc5NpnAkEAohj9apMfFok

Add a footer to the authenticated and admin app shells (previously none) and
make the public footer + auth header branding-driven instead of hardcoded.
- lib/brand.ts: single source for framework brand metadata + footer links
- BrandingMark: optional stacked caption (backward compatible)
- BrandingFooter: reusable footer (brand lockup + © year · MIT + links),
app/public variants
- SidebarLayout: flex-column main with sticky-bottom BrandingFooter
- PublicLayout: use shared BrandingFooter; nav links via BRAND_REPO_URL
- AuthCardShell: brand lockup now driven by the branding shared prop
Claude-Session: https://claude.ai/code/session_01GYc5NpnAkEAohj9apMfFok
- BrandingFooter: wrap BrandingMark in a flex container so the badge stays
attached to its wordmark (BrandingMark is a fragment; the footer's
justify-between row otherwise split the lockup into three items)
- BrandingMark: add size='lg' + badgeClassName so AuthCardShell keeps its
prominent 36px glowing badge (was silently shrunk to 32px/flat)
- lib/brand.ts: centralize BRAND_ACCENT gradient + BRAND_DEFAULT_APP_NAME;
use across BrandingFooter, AuthCardShell, PublicLayout (kills 3x dup +
unifies the unbranded fallback name)
Claude-Session: https://claude.ai/code/session_01GYc5NpnAkEAohj9apMfFok
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 25, 2026

Copy link
Copy Markdown

Deploying simple-module-python with Cloudflare Pages Cloudflare Pages

Latest commit:ead121d
Status: ✅ Deploy successful!
Preview URL:https://bd6de6f0.simple-module-python.pages.dev
Branch Preview URL:https://worktree-branding-header-foo.simple-module-python.pages.dev

View logs

Extend branding beyond the header/footer lockup so a configured deployment
is branded end to end:
- Derive the full --color-primary-50..900 ramp from the single brand hex so
the logo-badge gradient and auth mesh blobs follow the tenant colour, not
just bg-primary buttons (packages/ui/src/lib/color.ts + BrandingHead).
- Brand the document <title> on client navigations (lib/app-title + app.tsx)
and server-render title/favicon/theme-color into the root template before
hydration via a duck-typed branding_head() Jinja global (no SM009 coupling).
- Carry the app name in transactional email subjects/bodies, threaded through
a decoupled app_name provider (users stays independent of branding).
Tests: colour/app-title utils, branding_head, rendered shell (title +
theme-color hot-swap), and email branding. make lint + make test green.
Claude-Session: https://claude.ai/code/session_01GYc5NpnAkEAohj9apMfFok
@antosubashantosubash changed the title feat(branding): unify header & footer branding across all shellsfeat(branding): end-to-end branding — header, footer, colour ramp, title & emailsJun 26, 2026
- Reject control chars in app_name at the source (BrandingSettings + update
DTO via a shared clean_app_name helper) — a CR/LF previously passed a bare
strip() and would then break every transactional email's Subject header.
- Drop the server-rendered favicon <link> and its mirrored file_storage
route from framework code (SM009-adjacent coupling); the favicon is already
applied client-side by BrandingHead. branding_head() now surfaces only
plain settings strings (title + theme-color).
- BrandingHead: move setTitleAppName into an effect (no side-effect during
render) and keep the server-rendered theme-color meta in sync on a runtime
colour change, restoring it on unmount.
- color.ts: derive BASE_REFERENCE_CHROMA from the 600 ramp step instead of a
duplicated magic literal.
Tests: control-char rejection (settings + API 422), theme-color sync,
updated branding_head test. Affected suites green.
Claude-Session: https://claude.ai/code/session_01GYc5NpnAkEAohj9apMfFok
@antosubash
antosubash merged commit 6d1cfe8 into mainJun 26, 2026
12 checks passed
Sign up for freeto 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

@antosubash