Skip to content

feat(branding): multi-tenant org branding (logo, name, colours) with validation and AA contrast - #1653

Merged
Wilfred007 merged 2 commits into
Gildado:mainfrom
onejasonn:feat/1505-multi-tenant-branding
Sep 27, 2026
Merged

Wilfred007 merged 2 commits into
Gildado:mainfrom
onejasonn:feat/1505-multi-tenant-branding

Conversation

@onejasonn

@onejasonn onejasonn commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Organizations can now configure a custom logo, name, primary colour and accent colour that theme their PayD portal. ThemeProvider already stored an OrgBrandConfig and mapped it to --brand-* CSS tokens, but nothing let an org set it, logoUrl was never rendered, and brand values were written into CSS unvalidated. This PR closes those gaps.

Changes

1. Validation and contrast (src/utils/brand.ts, ThemeProvider)

  • Only valid hex colours (#rgb or #rrggbb, normalised) are written into --brand-primary, --brand-accent and --brand-header-bg. Anything else, such as red; background:url(x), is ignored and the default myworkpay-inspired tokens apply.
  • The new --brand-on-primary token is set to white or deep navy (#0b1b3a), whichever gives the higher WCAG contrast on the org's primary colour. Text on a brand fill stays readable whatever colour an org picks.
  • Logo URLs: only https: and raster data:image/(png|jpeg|gif|webp) URIs are accepted. javascript:, http: (mixed content) and SVG data URIs are rejected.
  • Org name: trimmed and capped at 60 characters.

2. Settings UI (src/components/BrandingSettings.tsx, in Settings)

  • Fields: organization name, logo URL, and primary and accent colours, each with a native colour picker plus a hex text input.
  • Live preview: a solid CTA button ("Run payroll") and an arrow-style secondary link ("View details →"), matching the myworkpay design language, plus the button text's contrast ratio with an AA or below-AA label.
  • Accessibility:
    • every input has a <label>
    • invalid fields get aria-invalid and a role="alert" message, and Save is disabled
    • saving announces "Branding saved." through a polite role="status"
    • all controls are keyboard-operable with visible focus-visible rings
  • Reset to PayD default clears the org branding.
  • Responsive: a single column at 375px and two columns from md. Uses the existing card glass, --surface-hi and --border-hi tokens, so it follows light and dark mode.

3. Branded header (src/components/BrandLogo.tsx, used in AppLayout)

  • Shows the org's logo (alt="<Org> logo") and/or name when configured, with the name truncated on narrow screens. A screen-reader-only ", powered by PayD" is included.
  • Falls back to the PayD wordmark when branding isn't configured, or if the logo fails to load (onError).

Tests

Notes

  • Branding is persisted per browser through the existing payd-org-brand storage used by ThemeProvider. When an org-settings API endpoint exists, the same setBrandConfig call can be fed from it so the branding follows the organization across devices.

Commits

  1. feat(branding): validate org brand values and keep AA contrast
  2. feat(branding): org branding settings and branded header logo

Add utils/brand.ts (hex validation/normalisation, WCAG contrast ratio,
readable text picker, https/raster-only logo sanitiser, org-name
sanitiser). ThemeProvider now only writes valid hex colours into the
--brand-* custom properties and sets --brand-on-primary to white or navy,
whichever keeps AA contrast on the org's primary colour.
- BrandingSettings (Settings page): organization name, https logo URL,
  primary and accent colour (picker + hex input), live preview with the
  button-text contrast ratio, inline validation (role=alert, aria-invalid),
  save with a polite status message, and reset to PayD defaults.
- BrandLogo in AppLayout's header shows the org logo and/or name when
  configured (falls back to the PayD wordmark, and on logo load errors).
- Tests cover save/theme, invalid-logo blocking and reset.
@drips-wave

drips-wave Bot commented Sep 27, 2026

Copy link
Copy Markdown

@onejasonn Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Wilfred007
Wilfred007 merged commit 84ab7e3 into Gildado:main Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants