Skip to content

feat(core): derive every brand token from the agency color by contrast - #204

Merged
SishirP17 merged 1 commit into
mainfrom
feat/agency-theme-resolver
Aug 18, 2026
Merged

feat(core): derive every brand token from the agency color by contrast#204
SishirP17 merged 1 commit into
mainfrom
feat/agency-theme-resolver

Conversation

@SishirP17

Copy link
Copy Markdown
Collaborator

applyAgencyTheme overrode three variables and left everything else at the RayHealth defaults:

  • --color-on-brand stayed #FFFFFF forever, so a pale agency brand color would produce white-on-pale text app-wide.
  • accentColor was read from the database and never applied.
  • The set and clear paths listed their variables separately, so they could drift.

This is the part that makes a future home-care agency's color scheme safe rather than lucky.

How it works

theme-resolver.ts derives all 25 brand tokens as a pure function of the agency's colors. Two properties do the work:

  • The foreground is chosen, not assumed. --color-on-brand is whichever of white or slate-900 scores higher — measured against every stop of --gradient-brand, not just --color-primary, because a gradient-filled button shows all three.
  • The fill moves, not the ink. A color that cannot carry a label is deepened until it can, and the agency's literal color is preserved as --color-brand-raw for logo marks and other text-free surfaces, so the branding survives.
#107480 → primary #107480   (untouched)
#1A5FA8 → primary #1A5FA8   (untouched)
#EE6C2C → primary #CE4C10   "deepened so labels stay readable; original kept for logo/gradient"
#FFD400 → primary #877000
#FFFFFF → primary #737373

Alpha tints solve for their composited result: --color-sidebar-active lowers its alpha until white still clears AA over the dark rail, and the focus ring raises its alpha until it is actually visible on the surface.

resolveAgencyTheme is total — {}, null, garbage, and attempted CSS injection all return a complete AA-clean set. Every emitted value is built from numbers, so no caller-supplied text reaches the CSSOM. 3,753 assertions over 416 brand colors, including pure white, pure black, and mid-grey #808080, which fails against both white and black ink.

Boundary

agencyThemeSchema validates the colors instead of typing them as bare z.string(). findTheme salvages field by field rather than all-or-nothing, so a legacy row with one bad hex keeps its brand name — read-time normalization, no migration.

Two things worth knowing

  • The web bundle deep-imports the resolver rather than the package barrel. The barrel re-exports every repository and would drag knex, pg, and ssh2-sftp-client into the browser. Verified absent from the built bundle.
  • vitest.config.ts needs its own @rayhealth/core alias because it replaces vite.config.ts rather than merging with it.

Third of five, stacked on #203.

@SishirP17
SishirP17 requested a review from durga710 as a code owner August 18, 2026 22:42
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rayhealth-evv-platform-app Ready Ready Preview Aug 18, 2026 11:01pm

applyAgencyTheme overrode three variables and left everything else at the
RayHealth defaults. --color-on-brand stayed white forever, so a pale agency
brand color would produce white-on-pale text app-wide; accentColor was read
from the database and never applied; and the set and clear paths listed
their variables separately, so they could drift.

theme-resolver.ts derives the whole brand token set as a pure function of
the agency's colors. Two properties do the work:

  - The foreground is chosen, not assumed. --color-on-brand is whichever of
    white or slate-900 scores higher, measured against every stop of
    --gradient-brand rather than against --color-primary alone.
  - The fill moves, not the ink. A color that cannot carry a label is
    deepened until it can, and the agency's literal color is preserved as
    --color-brand-raw for logo marks and other text-free surfaces, so
    branding survives.

Alpha tints solve for their composited result, so --color-sidebar-active
lowers its alpha until white still clears AA over the dark rail, and the
focus ring raises its alpha until it is visible on the surface.

resolveAgencyTheme is total: {}, null, garbage, and attempted CSS injection
all return a complete AA-clean set. Every emitted value is built from
numbers, so no caller-supplied text reaches the CSSOM. 3,753 assertions
cover 416 brand colors including pure white, pure black, and mid-grey,
which fails against both white and black ink.

agencyThemeSchema now validates the colors instead of typing them as bare
strings, and findTheme salvages field by field so a legacy row with one bad
hex keeps its brand name rather than being dropped whole.

The web bundle deep-imports the resolver rather than the package barrel,
which re-exports every repository and would drag knex, pg, and
ssh2-sftp-client into the browser. vitest.config.ts needs its own alias
because it replaces vite.config.ts rather than merging with it.

This is also the first time packages/web imports from packages/core, so web
now declares the dependency. It resolved fine locally through the vite
alias and the tsconfig project reference, but turbo builds its graph from
package.json, so `^build` never built core before web's typecheck and CI
failed with TS6305 on a missing core/dist. Verified from a cold state with
core/dist and the turbo cache both removed.
@SishirP17
SishirP17 force-pushed the feat/agency-theme-resolver branch from ca897cf to 6031d51 Compare August 18, 2026 23:00
@SishirP17
SishirP17 merged commit 5d7829c into main Aug 18, 2026
14 checks passed
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