Skip to content

Identity theme tokens

What this is: how the Identity service's Razor pages get the same colours and type as the Angular app. It is the token pipeline from account portal slice 3 (plan), and it delivers FEAT-023 Wave 6's "reconcile shared source values with Identity" (technical plan).

The pipeline

src/services/web/src/global-styles/_m3-palettes.scss   (ng generate @angular/material:theme-color)
src/services/web/src/global-styles/syrf-palette.scss   (SyRF brand palette)
                │
                ▼
src/services/web/src/global-styles/_shared-theme-sources.scss
    system-tokens(light|dark)  mat.theme() with the app's palettes, Roboto, density 0
    brand-ink                  --syrf-brand-ink, --syrf-on-brand-ink
    link-and-focus(light|dark) --syrf-link, --syrf-focus-ring
src/services/web/src/app/shared/information-box/_information-box-tokens.scss
    light / dark               --syrf-information-box-{info,alert,warning,success}-*
                │
      ┌─────────┴───────────────────────────────┐
      ▼                                         ▼
syrf-theme.scss + information-box (the app)     tools/identity-tokens/identity-tokens.scss
      --mat-sys-*, --syrf-*                         │  generate-identity-tokens.mjs
                                                    │  renames --mat-sys-* → --syrf-sys-*
                                                    ▼
             src/services/identity/SyRF.Identity.Endpoint/wwwroot/css/syrf-tokens.generated.css
                                    (checked in; "GENERATED FILE. DO NOT EDIT BY HAND.")
                                                    │
                                                    ▼
                                         wwwroot/css/syrf-auth.css (var(--syrf-sys-*) only)
  • One source. The app's theme and the generator include the same Sass mixins. Nothing is copied: changing a palette tone, the typography, a brand role or a notice colour changes both surfaces.
  • No Angular role names on Identity. The generator renames --mat-sys-* to --syrf-sys-*, so Identity consumes reviewed source values without owning or overriding an Angular runtime role (FEAT-023 principle 3). The generator fails if any --mat- name survives the rename.
  • No build coupling. The output is checked in. The Identity image build stays .NET-only and needs no Node; the web build never reads Identity CSS.
  • Dark mode. The generated file emits the dark layer twice: under @media (prefers-color-scheme: dark) :root:not([data-theme=light]) (system preference) and under :root[data-theme=dark] (an explicit choice the server rendered from the shared theme preference).
  • Identity-only extras. One value has no app counterpart: --syrf-on-brand-ink-variant (80% of the on-brand ink) for secondary copy on the navy brand panel and header. It is derived from the same $on-brand-ink-base.

Commands

From src/services/web:

pnpm run generate:identity-tokens   # rewrite the checked-in file
pnpm run check:identity-tokens      # exit 1 if it is stale

Checks

Check Where Fails when
Drift (--check) src/global-styles/identity-tokens.spec.ts, in the web CI lane (pnpm exec ng test) the checked-in file differs from a fresh compile, e.g. after a palette edit without regeneration
Mutation the same spec a one-tone edit to _m3-palettes.scss in a scratch copy does not make the check fail
Parity the same spec any --syrf-sys-* differs from the app's --mat-sys-* in light or dark, or a notice colour differs from the app component's
Notice contrast the same spec a notice's text is below 4.5:1 on its container, light or dark
Hand edit Identity IdentityThemeTokenTests the body no longer matches the Body-SHA256 recorded in the header
Colour literals Identity IdentityThemeTokenTests syrf-auth.css contains a hex/rgb() literal outside its commented allowlist (Google's fixed brand colours, on rules marked ALLOWLIST: Google) or reads a custom property that neither file defines
Rendered parity e2e/harness/identity-visual (manual) in Chromium, a role, a type style or a notice's computed style differs between Identity and the app, a notice pixel diff exceeds 1%, axe finds a serious or critical violation, or a page requests another origin

The web CI lane runs only when src/services/web/ changes, which is where every source lives. A hand edit of the generated file alone is caught by the Identity lane's hash check instead.

Visual harness

e2e/harness/identity-visual/run-identity-visual.mjs renders the restyled pages in Chromium next to the app's real theme and notice component, and writes screenshots plus report.json:

SYRF_IDENTITY_SNAPSHOT_DIR=/tmp/identity-snap dotnet test \
  src/services/identity/SyRF.Identity.Endpoint.Tests \
  --filter FullyQualifiedName~IdentityVisualSnapshotExporter
AXE_CORE=/path/to/axe-core/axe.min.js \
  node e2e/harness/identity-visual/run-identity-visual.mjs /tmp/identity-snap /tmp/identity-shots

The exporter test renders the pages through the real Identity host and does nothing unless the variable is set. AXE_CORE is optional (for example from npm pack axe-core).

Changing a value

  1. Change the source (_m3-palettes.scss, _shared-theme-sources.scss or _information-box-tokens.scss), never the generated file.
  2. Run pnpm run generate:identity-tokens and commit both.
  3. Run pnpm run check:contrast and the theme guard specs as for any theme change (src/services/web/CLAUDE.md, "Material 3 boundary" critical rule).