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¶
- Change the source (
_m3-palettes.scss,_shared-theme-sources.scssor_information-box-tokens.scss), never the generated file. - Run
pnpm run generate:identity-tokensand commit both. - Run
pnpm run check:contrastand the theme guard specs as for any theme change (src/services/web/CLAUDE.md, "Material 3 boundary" critical rule).