ps-helix 5.1.2 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/THEME.md CHANGED
@@ -1,446 +1,446 @@
1
- # Theme Customization Guide
2
-
3
- This guide explains how to customize the colors and theme of the Helix Design System in your application.
4
-
5
- ## Table of Contents
6
-
7
- - [Overview](#overview)
8
- - [Quick Start](#quick-start)
9
- - [Theme Service](#theme-service)
10
- - [Custom Colors with Injection Token](#custom-colors-with-injection-token)
11
- - [Accessibility Safeguard (WCAG)](#accessibility-safeguard-wcag)
12
- - [Available CSS Variables](#available-css-variables)
13
- - [Dynamic Color Changes](#dynamic-color-changes)
14
- - [Examples](#examples)
15
-
16
- ## Overview
17
-
18
- The Helix Design System uses a signal-based theming system that supports:
19
-
20
- - Light and dark themes
21
- - Custom primary and secondary brand colors (customer context)
22
- - Automatic variant generation in OKLCH (hue and chroma preserved, lightness adjusted per mode)
23
- - WCAG contrast safeguard: a brand color that does not meet the target contrast ratio against the current theme background is silently adjusted before being applied to UI components
24
- - Dynamic theme switching at runtime
25
- - Theme preference persistence in `localStorage` (key: `helix-theme-preference`)
26
-
27
- ## Quick Start
28
-
29
- ### Basic Theme Switching
30
-
31
- Import and use the `ThemeService` to toggle between light and dark themes:
32
-
33
- ```typescript
34
- import { Component, inject } from '@angular/core';
35
- import { ThemeService } from 'ps-helix';
36
-
37
- @Component({
38
- selector: 'app-root',
39
- template: `
40
- <button (click)="toggleTheme()">
41
- Current theme: {{ themeService.themeName() }}
42
- </button>
43
- `
44
- })
45
- export class AppComponent {
46
- themeService = inject(ThemeService);
47
-
48
- toggleTheme() {
49
- this.themeService.toggleTheme();
50
- }
51
- }
52
- ```
53
-
54
- ## Theme Service
55
-
56
- The `ThemeService` provides the following API:
57
-
58
- ### Methods
59
-
60
- - `setDarkTheme(isDark: boolean)` - Set the theme to dark or light
61
- - `toggleTheme()` - Toggle between light and dark themes
62
- - `updateTheme(name: Theme)` - Update theme by name (`'light'` or `'dark'`) and persist it in localStorage
63
- - `applyCustomerTheme()` - Re-apply brand colors from the customer context (usually called automatically after theme changes or when the context updates)
64
-
65
- ### Computed Signals
66
-
67
- - `themeName()` - Returns current theme name (`'light'` or `'dark'`)
68
- - `isDarkTheme()` - Returns a boolean indicating whether dark theme is active
69
- - `themeInfo()` - Returns complete theme information including the last change date
70
-
71
- ### Types
72
-
73
- ```typescript
74
- export type Theme = 'light' | 'dark';
75
-
76
- export interface ThemeConfig {
77
- isDark: boolean;
78
- name: Theme;
79
- customerTheme?: {
80
- primaryColor: string;
81
- secondaryColor?: string;
82
- };
83
- }
84
- ```
85
-
86
- ## Default Colors
87
-
88
- If you don't provide custom colors, the design system falls back to the CSS defaults defined in the theme files:
89
-
90
- - Light mode primary: `#0B0191`
91
- - Dark mode primary: `#8178F7`
92
- - Light mode secondary: `#5E5E5E`
93
- - Dark mode secondary: `#5B5A5A`
94
-
95
- These defaults are defined in `projects/ps-helix/src/lib/styles/themes/light.css` and `dark.css`.
96
-
97
- ## Custom Colors with Injection Token
98
-
99
- To customize the primary and secondary colors of your design system to match your brand, provide a service that implements `CustomerContextService` through the `CUSTOMER_CONTEXT_SERVICE` injection token.
100
-
101
- ### Why an Injection Token and not just CSS variables?
102
-
103
- 1. Variants (light, lighter, dark, darker) are computed in OKLCH at runtime and depend on the current theme (light vs dark).
104
- 2. Text color on top of brand colors is computed from real WCAG contrast ratios.
105
- 3. Brand colors are verified against a minimum contrast ratio and silently adjusted for UI usage when needed; the original color is still exposed as `-source` for decorative contexts.
106
- 4. The token keeps everything type-safe and reactive via Angular DI.
107
-
108
- ### Step 1: Create a Customer Context Service
109
-
110
- ```typescript
111
- // src/app/services/app-theme-context.service.ts
112
- import { Injectable, signal } from '@angular/core';
113
- import { CustomerContextService } from 'ps-helix';
114
-
115
- @Injectable({ providedIn: 'root' })
116
- export class AppThemeContextService implements CustomerContextService {
117
- private primaryColorSignal = signal('#FF0000');
118
- private secondaryColorSignal = signal('#00AA00');
119
-
120
- primaryColor = this.primaryColorSignal.asReadonly();
121
- secondaryColor = this.secondaryColorSignal.asReadonly();
122
-
123
- setPrimaryColor(color: string) {
124
- this.primaryColorSignal.set(color);
125
- }
126
-
127
- setSecondaryColor(color: string) {
128
- this.secondaryColorSignal.set(color);
129
- }
130
- }
131
- ```
132
-
133
- The `CustomerContextService` interface requires two methods:
134
-
135
- - `primaryColor(): string` - Returns the primary brand color (hex)
136
- - `secondaryColor(): string` - Returns the secondary brand color (hex)
137
-
138
- ### Step 2: Provide the Service with the Injection Token
139
-
140
- ```typescript
141
- // src/app/app.config.ts
142
- import { ApplicationConfig } from '@angular/core';
143
- import { provideRouter } from '@angular/router';
144
- import { CUSTOMER_CONTEXT_SERVICE } from 'ps-helix';
145
- import { AppThemeContextService } from './services/app-theme-context.service';
146
- import { routes } from './app.routes';
147
-
148
- export const appConfig: ApplicationConfig = {
149
- providers: [
150
- provideRouter(routes),
151
- {
152
- provide: CUSTOMER_CONTEXT_SERVICE,
153
- useExisting: AppThemeContextService
154
- }
155
- ]
156
- };
157
- ```
158
-
159
- ### Step 3: Re-apply the Theme (Optional)
160
-
161
- ```typescript
162
- import { Component, inject, OnInit } from '@angular/core';
163
- import { ThemeService } from 'ps-helix';
164
-
165
- @Component({
166
- selector: 'app-root',
167
- template: `<router-outlet />`
168
- })
169
- export class AppComponent implements OnInit {
170
- private themeService = inject(ThemeService);
171
-
172
- ngOnInit() {
173
- this.themeService.applyCustomerTheme();
174
- }
175
- }
176
- ```
177
-
178
- ### Alternative: Simple Non-Reactive Service
179
-
180
- ```typescript
181
- import { Injectable } from '@angular/core';
182
- import { CustomerContextService } from 'ps-helix';
183
-
184
- @Injectable({ providedIn: 'root' })
185
- export class AppThemeContextService implements CustomerContextService {
186
- primaryColor() { return '#FF0000'; }
187
- secondaryColor() { return '#00AA00'; }
188
- }
189
- ```
190
-
191
- ## Accessibility Safeguard (WCAG)
192
-
193
- When `applyCustomerTheme()` runs, each brand color is evaluated against the current theme background:
194
-
195
- - Contrast is computed using the true WCAG 2.1 relative luminance formula.
196
- - If the ratio is below the target (AA by default, AAA if configured), the color is adjusted in OKLCH: the luminance is shifted (darker on light backgrounds, lighter on dark backgrounds) while hue and chroma are preserved as much as possible.
197
- - The adjusted color is written into `--customer-primary-color` / `--customer-secondary-color` and consumed by every UI component.
198
- - The original, unmodified brand color is written into `--customer-primary-color-source` / `--customer-secondary-color-source`, available for decorative surfaces (logos, marketing images) where the contrast rule does not apply.
199
- - The service never logs or warns: the adjustment is silent by design.
200
-
201
- ### Configuring the Target Ratio
202
-
203
- Use the `PSH_THEME_OPTIONS` injection token to opt into AAA:
204
-
205
- ```typescript
206
- import { PSH_THEME_OPTIONS, PshThemeOptions } from 'ps-helix';
207
-
208
- export const appConfig: ApplicationConfig = {
209
- providers: [
210
- {
211
- provide: PSH_THEME_OPTIONS,
212
- useValue: { targetContrast: 'AAA' } satisfies PshThemeOptions
213
- }
214
- ]
215
- };
216
- ```
217
-
218
- Default is `'AA'` (ratio 4.5:1). `'AAA'` raises the requirement to 7:1.
219
-
220
- ### OKLCH Variant Derivation
221
-
222
- The four tonal variants (`light`, `lighter`, `dark`, `darker`) are generated by shifting the OKLCH lightness of the accessible base color with mode-aware deltas:
223
-
224
- | Mode | light | lighter | dark | darker |
225
- |-------|-------|---------|-------|--------|
226
- | Light | +0.08 | +0.18 | -0.08 | -0.16 |
227
- | Dark | +0.06 | +0.14 | -0.06 | -0.14 |
228
-
229
- Hue and chroma are preserved so the color palette stays perceptually consistent. Fixed percentage RGB lightening/darkening is no longer used.
230
-
231
- ## Available CSS Variables
232
-
233
- Once configured, `ThemeService` writes the following CSS variables on `:root`:
234
-
235
- ### Primary Color Variables
236
-
237
- - `--customer-primary-color` - Accessibility-adjusted primary color (used by components)
238
- - `--customer-primary-color-source` - Original brand color, unmodified (for decorative use only)
239
- - `--customer-primary-color-light` - OKLCH-derived lighter variant
240
- - `--customer-primary-color-lighter` - OKLCH-derived extra light variant
241
- - `--customer-primary-color-dark` - OKLCH-derived darker variant
242
- - `--customer-primary-color-darker` - OKLCH-derived extra dark variant
243
- - `--customer-primary-color-text` - Readable text color (black or white) picked via WCAG contrast against the adjusted primary
244
- - `--customer-primary-color-rgb` - `r, g, b` triplet of the adjusted primary for `rgba()` usage
245
-
246
- ### Secondary Color Variables
247
-
248
- - `--customer-secondary-color`
249
- - `--customer-secondary-color-source`
250
- - `--customer-secondary-color-light`
251
- - `--customer-secondary-color-lighter`
252
- - `--customer-secondary-color-dark`
253
- - `--customer-secondary-color-darker`
254
- - `--customer-secondary-color-text`
255
- - `--customer-secondary-color-rgb`
256
-
257
- ### Component Usage
258
-
259
- Design system components consume the stable abstract variables, which fall back to the defaults when no customer context is provided:
260
-
261
- ```css
262
- /* From projects/ps-helix/src/lib/styles/themes/light.css */
263
- --primary-color: var(--customer-primary-color, #0B0191);
264
- --primary-color-light: var(--customer-primary-color-light, #0F02C4);
265
- ```
266
-
267
- ### Using in Your Custom Styles
268
-
269
- Use the abstract variables (`--primary-color`, `--secondary-color`, ...) in your own components so you automatically inherit the accessibility-adjusted palette:
270
-
271
- ```css
272
- .my-custom-button {
273
- background-color: var(--primary-color);
274
- color: var(--primary-color-text);
275
- }
276
-
277
- .my-custom-button:hover {
278
- background-color: var(--primary-color-light);
279
- }
280
- ```
281
-
282
- For decorative brand elements that must match the literal brand color (e.g. a logo background), use the `-source` variable:
283
-
284
- ```css
285
- .brand-logo-surface {
286
- background-color: var(--customer-primary-color-source);
287
- }
288
- ```
289
-
290
- ## Dynamic Color Changes
291
-
292
- Since the customer context service uses signals, you can change colors at runtime:
293
-
294
- ```typescript
295
- import { Component, inject } from '@angular/core';
296
- import { ThemeService } from 'ps-helix';
297
- import { AppThemeContextService } from './services/app-theme-context.service';
298
-
299
- @Component({
300
- selector: 'app-theme-switcher',
301
- template: `
302
- <button (click)="setRedTheme()">Red</button>
303
- <button (click)="setBlueTheme()">Blue</button>
304
- <button (click)="setGreenTheme()">Green</button>
305
- `
306
- })
307
- export class ThemeSwitcherComponent {
308
- private themeContext = inject(AppThemeContextService);
309
- private themeService = inject(ThemeService);
310
-
311
- setRedTheme() {
312
- this.themeContext.setPrimaryColor('#DC2626');
313
- this.themeContext.setSecondaryColor('#EF4444');
314
- this.themeService.applyCustomerTheme();
315
- }
316
-
317
- setBlueTheme() {
318
- this.themeContext.setPrimaryColor('#2563EB');
319
- this.themeContext.setSecondaryColor('#3B82F6');
320
- this.themeService.applyCustomerTheme();
321
- }
322
-
323
- setGreenTheme() {
324
- this.themeContext.setPrimaryColor('#16A34A');
325
- this.themeContext.setSecondaryColor('#22C55E');
326
- this.themeService.applyCustomerTheme();
327
- }
328
- }
329
- ```
330
-
331
- ## Examples
332
-
333
- ### Example 1: Brand Colors from Configuration
334
-
335
- ```typescript
336
- import { Injectable, signal } from '@angular/core';
337
- import { environment } from '../environments/environment';
338
-
339
- @Injectable({ providedIn: 'root' })
340
- export class AppThemeContextService {
341
- private primaryColorSignal = signal(environment.brandPrimaryColor);
342
- private secondaryColorSignal = signal(environment.brandSecondaryColor);
343
-
344
- primaryColor = this.primaryColorSignal.asReadonly();
345
- secondaryColor = this.secondaryColorSignal.asReadonly();
346
- }
347
- ```
348
-
349
- ### Example 2: User-Selected Theme
350
-
351
- ```typescript
352
- import { Injectable, signal, effect } from '@angular/core';
353
-
354
- @Injectable({ providedIn: 'root' })
355
- export class AppThemeContextService {
356
- private primaryColorSignal = signal(this.loadFromStorage('primaryColor', '#0B0191'));
357
- private secondaryColorSignal = signal(this.loadFromStorage('secondaryColor', '#5E5E5E'));
358
-
359
- primaryColor = this.primaryColorSignal.asReadonly();
360
- secondaryColor = this.secondaryColorSignal.asReadonly();
361
-
362
- constructor() {
363
- effect(() => {
364
- localStorage.setItem('primaryColor', this.primaryColorSignal());
365
- localStorage.setItem('secondaryColor', this.secondaryColorSignal());
366
- });
367
- }
368
-
369
- setPrimaryColor(color: string) { this.primaryColorSignal.set(color); }
370
- setSecondaryColor(color: string) { this.secondaryColorSignal.set(color); }
371
-
372
- private loadFromStorage(key: string, defaultValue: string): string {
373
- try { return localStorage.getItem(key) || defaultValue; }
374
- catch { return defaultValue; }
375
- }
376
- }
377
- ```
378
-
379
- ### Example 3: Multi-Tenant Application
380
-
381
- ```typescript
382
- import { Injectable, signal } from '@angular/core';
383
-
384
- interface TenantTheme {
385
- primary: string;
386
- secondary: string;
387
- }
388
-
389
- const TENANT_THEMES: Record<string, TenantTheme> = {
390
- 'tenant-a': { primary: '#DC2626', secondary: '#EF4444' },
391
- 'tenant-b': { primary: '#2563EB', secondary: '#3B82F6' },
392
- 'tenant-c': { primary: '#16A34A', secondary: '#22C55E' }
393
- };
394
-
395
- @Injectable({ providedIn: 'root' })
396
- export class AppThemeContextService {
397
- private currentTenantId = signal<string>('tenant-a');
398
- private primaryColorSignal = signal(TENANT_THEMES['tenant-a'].primary);
399
- private secondaryColorSignal = signal(TENANT_THEMES['tenant-a'].secondary);
400
-
401
- primaryColor = this.primaryColorSignal.asReadonly();
402
- secondaryColor = this.secondaryColorSignal.asReadonly();
403
-
404
- setTenant(tenantId: string) {
405
- const theme = TENANT_THEMES[tenantId];
406
- if (theme) {
407
- this.currentTenantId.set(tenantId);
408
- this.primaryColorSignal.set(theme.primary);
409
- this.secondaryColorSignal.set(theme.secondary);
410
- }
411
- }
412
- }
413
- ```
414
-
415
- ## Troubleshooting
416
-
417
- ### Colors not applying
418
-
419
- 1. Verify that your service is provided with the `CUSTOMER_CONTEXT_SERVICE` token in your app config.
420
- 2. Confirm your service implements `primaryColor()` and `secondaryColor()` returning valid hex strings.
421
- 3. `ThemeService` is silent by design; if a brand color looks different from the one you provided, it is because the WCAG safeguard adjusted it. Read the adjusted color via `getComputedStyle(document.documentElement).getPropertyValue('--customer-primary-color')` and the original via `--customer-primary-color-source`.
422
-
423
- ### Default colors showing instead of custom colors
424
-
425
- - The context service is not provided, or
426
- - `primaryColor()` / `secondaryColor()` return an invalid hex string.
427
-
428
- ### Colors not updating dynamically
429
-
430
- 1. Make sure you call `this.themeService.applyCustomerTheme()` after changing a color.
431
- 2. Verify that your signals are actually updating.
432
- 3. Confirm `ThemeService` is injected in the component driving the change.
433
-
434
- ## Best Practices
435
-
436
- 1. Use signals in your customer context service for reactivity.
437
- 2. Provide valid hex colors (`#RRGGBB`).
438
- 3. Trust the WCAG safeguard; do not hand-tune base colors for contrast.
439
- 4. For decorative brand surfaces, use `--customer-*-color-source`. For every other UI use case, use the abstract variables (`--primary-color`, ...).
440
- 5. Persist user preferences in `localStorage` when relevant.
441
- 6. Document brand color choices alongside your app's style guide.
442
-
443
- ## Related Documentation
444
-
445
- - [Component Documentation](./README.md)
446
- - [Translation Guide](./lib/services/translation/README.md)
1
+ # Theme Customization Guide
2
+
3
+ This guide explains how to customize the colors and theme of the Helix Design System in your application.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Quick Start](#quick-start)
9
+ - [Theme Service](#theme-service)
10
+ - [Custom Colors with Injection Token](#custom-colors-with-injection-token)
11
+ - [Accessibility Safeguard (WCAG)](#accessibility-safeguard-wcag)
12
+ - [Available CSS Variables](#available-css-variables)
13
+ - [Dynamic Color Changes](#dynamic-color-changes)
14
+ - [Examples](#examples)
15
+
16
+ ## Overview
17
+
18
+ The Helix Design System uses a signal-based theming system that supports:
19
+
20
+ - Light and dark themes
21
+ - Custom primary and secondary brand colors (customer context)
22
+ - Automatic variant generation in OKLCH (hue and chroma preserved, lightness adjusted per mode)
23
+ - WCAG contrast safeguard: a brand color that does not meet the target contrast ratio against the current theme background is silently adjusted before being applied to UI components
24
+ - Dynamic theme switching at runtime
25
+ - Theme preference persistence in `localStorage` (key: `helix-theme-preference`)
26
+
27
+ ## Quick Start
28
+
29
+ ### Basic Theme Switching
30
+
31
+ Import and use the `ThemeService` to toggle between light and dark themes:
32
+
33
+ ```typescript
34
+ import { Component, inject } from '@angular/core';
35
+ import { ThemeService } from 'ps-helix';
36
+
37
+ @Component({
38
+ selector: 'app-root',
39
+ template: `
40
+ <button (click)="toggleTheme()">
41
+ Current theme: {{ themeService.themeName() }}
42
+ </button>
43
+ `
44
+ })
45
+ export class AppComponent {
46
+ themeService = inject(ThemeService);
47
+
48
+ toggleTheme() {
49
+ this.themeService.toggleTheme();
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Theme Service
55
+
56
+ The `ThemeService` provides the following API:
57
+
58
+ ### Methods
59
+
60
+ - `setDarkTheme(isDark: boolean)` - Set the theme to dark or light
61
+ - `toggleTheme()` - Toggle between light and dark themes
62
+ - `updateTheme(name: Theme)` - Update theme by name (`'light'` or `'dark'`) and persist it in localStorage
63
+ - `applyCustomerTheme()` - Re-apply brand colors from the customer context (usually called automatically after theme changes or when the context updates)
64
+
65
+ ### Computed Signals
66
+
67
+ - `themeName()` - Returns current theme name (`'light'` or `'dark'`)
68
+ - `isDarkTheme()` - Returns a boolean indicating whether dark theme is active
69
+ - `themeInfo()` - Returns complete theme information including the last change date
70
+
71
+ ### Types
72
+
73
+ ```typescript
74
+ export type Theme = 'light' | 'dark';
75
+
76
+ export interface ThemeConfig {
77
+ isDark: boolean;
78
+ name: Theme;
79
+ customerTheme?: {
80
+ primaryColor: string;
81
+ secondaryColor?: string;
82
+ };
83
+ }
84
+ ```
85
+
86
+ ## Default Colors
87
+
88
+ If you don't provide custom colors, the design system falls back to the CSS defaults defined in the theme files:
89
+
90
+ - Light mode primary: `#0B0191`
91
+ - Dark mode primary: `#8178F7`
92
+ - Light mode secondary: `#5E5E5E`
93
+ - Dark mode secondary: `#5B5A5A`
94
+
95
+ These defaults are defined in `projects/ps-helix/src/lib/styles/themes/light.css` and `dark.css`.
96
+
97
+ ## Custom Colors with Injection Token
98
+
99
+ To customize the primary and secondary colors of your design system to match your brand, provide a service that implements `CustomerContextService` through the `CUSTOMER_CONTEXT_SERVICE` injection token.
100
+
101
+ ### Why an Injection Token and not just CSS variables?
102
+
103
+ 1. Variants (light, lighter, dark, darker) are computed in OKLCH at runtime and depend on the current theme (light vs dark).
104
+ 2. Text color on top of brand colors is computed from real WCAG contrast ratios.
105
+ 3. Brand colors are verified against a minimum contrast ratio and silently adjusted for UI usage when needed; the original color is still exposed as `-source` for decorative contexts.
106
+ 4. The token keeps everything type-safe and reactive via Angular DI.
107
+
108
+ ### Step 1: Create a Customer Context Service
109
+
110
+ ```typescript
111
+ // src/app/services/app-theme-context.service.ts
112
+ import { Injectable, signal } from '@angular/core';
113
+ import { CustomerContextService } from 'ps-helix';
114
+
115
+ @Injectable({ providedIn: 'root' })
116
+ export class AppThemeContextService implements CustomerContextService {
117
+ private primaryColorSignal = signal('#FF0000');
118
+ private secondaryColorSignal = signal('#00AA00');
119
+
120
+ primaryColor = this.primaryColorSignal.asReadonly();
121
+ secondaryColor = this.secondaryColorSignal.asReadonly();
122
+
123
+ setPrimaryColor(color: string) {
124
+ this.primaryColorSignal.set(color);
125
+ }
126
+
127
+ setSecondaryColor(color: string) {
128
+ this.secondaryColorSignal.set(color);
129
+ }
130
+ }
131
+ ```
132
+
133
+ The `CustomerContextService` interface requires two methods:
134
+
135
+ - `primaryColor(): string` - Returns the primary brand color (hex)
136
+ - `secondaryColor(): string` - Returns the secondary brand color (hex)
137
+
138
+ ### Step 2: Provide the Service with the Injection Token
139
+
140
+ ```typescript
141
+ // src/app/app.config.ts
142
+ import { ApplicationConfig } from '@angular/core';
143
+ import { provideRouter } from '@angular/router';
144
+ import { CUSTOMER_CONTEXT_SERVICE } from 'ps-helix';
145
+ import { AppThemeContextService } from './services/app-theme-context.service';
146
+ import { routes } from './app.routes';
147
+
148
+ export const appConfig: ApplicationConfig = {
149
+ providers: [
150
+ provideRouter(routes),
151
+ {
152
+ provide: CUSTOMER_CONTEXT_SERVICE,
153
+ useExisting: AppThemeContextService
154
+ }
155
+ ]
156
+ };
157
+ ```
158
+
159
+ ### Step 3: Re-apply the Theme (Optional)
160
+
161
+ ```typescript
162
+ import { Component, inject, OnInit } from '@angular/core';
163
+ import { ThemeService } from 'ps-helix';
164
+
165
+ @Component({
166
+ selector: 'app-root',
167
+ template: `<router-outlet />`
168
+ })
169
+ export class AppComponent implements OnInit {
170
+ private themeService = inject(ThemeService);
171
+
172
+ ngOnInit() {
173
+ this.themeService.applyCustomerTheme();
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Alternative: Simple Non-Reactive Service
179
+
180
+ ```typescript
181
+ import { Injectable } from '@angular/core';
182
+ import { CustomerContextService } from 'ps-helix';
183
+
184
+ @Injectable({ providedIn: 'root' })
185
+ export class AppThemeContextService implements CustomerContextService {
186
+ primaryColor() { return '#FF0000'; }
187
+ secondaryColor() { return '#00AA00'; }
188
+ }
189
+ ```
190
+
191
+ ## Accessibility Safeguard (WCAG)
192
+
193
+ When `applyCustomerTheme()` runs, each brand color is evaluated against the current theme background:
194
+
195
+ - Contrast is computed using the true WCAG 2.1 relative luminance formula.
196
+ - If the ratio is below the target (AA by default, AAA if configured), the color is adjusted in OKLCH: the luminance is shifted (darker on light backgrounds, lighter on dark backgrounds) while hue and chroma are preserved as much as possible.
197
+ - The adjusted color is written into `--customer-primary-color` / `--customer-secondary-color` and consumed by every UI component.
198
+ - The original, unmodified brand color is written into `--customer-primary-color-source` / `--customer-secondary-color-source`, available for decorative surfaces (logos, marketing images) where the contrast rule does not apply.
199
+ - The service never logs or warns: the adjustment is silent by design.
200
+
201
+ ### Configuring the Target Ratio
202
+
203
+ Use the `PSH_THEME_OPTIONS` injection token to opt into AAA:
204
+
205
+ ```typescript
206
+ import { PSH_THEME_OPTIONS, PshThemeOptions } from 'ps-helix';
207
+
208
+ export const appConfig: ApplicationConfig = {
209
+ providers: [
210
+ {
211
+ provide: PSH_THEME_OPTIONS,
212
+ useValue: { targetContrast: 'AAA' } satisfies PshThemeOptions
213
+ }
214
+ ]
215
+ };
216
+ ```
217
+
218
+ Default is `'AA'` (ratio 4.5:1). `'AAA'` raises the requirement to 7:1.
219
+
220
+ ### OKLCH Variant Derivation
221
+
222
+ The four tonal variants (`light`, `lighter`, `dark`, `darker`) are generated by shifting the OKLCH lightness of the accessible base color with mode-aware deltas:
223
+
224
+ | Mode | light | lighter | dark | darker |
225
+ |-------|-------|---------|-------|--------|
226
+ | Light | +0.08 | +0.18 | -0.08 | -0.16 |
227
+ | Dark | +0.06 | +0.14 | -0.06 | -0.14 |
228
+
229
+ Hue and chroma are preserved so the color palette stays perceptually consistent. Fixed percentage RGB lightening/darkening is no longer used.
230
+
231
+ ## Available CSS Variables
232
+
233
+ Once configured, `ThemeService` writes the following CSS variables on `:root`:
234
+
235
+ ### Primary Color Variables
236
+
237
+ - `--customer-primary-color` - Accessibility-adjusted primary color (used by components)
238
+ - `--customer-primary-color-source` - Original brand color, unmodified (for decorative use only)
239
+ - `--customer-primary-color-light` - OKLCH-derived lighter variant
240
+ - `--customer-primary-color-lighter` - OKLCH-derived extra light variant
241
+ - `--customer-primary-color-dark` - OKLCH-derived darker variant
242
+ - `--customer-primary-color-darker` - OKLCH-derived extra dark variant
243
+ - `--customer-primary-color-text` - Readable text color (black or white) picked via WCAG contrast against the adjusted primary
244
+ - `--customer-primary-color-rgb` - `r, g, b` triplet of the adjusted primary for `rgba()` usage
245
+
246
+ ### Secondary Color Variables
247
+
248
+ - `--customer-secondary-color`
249
+ - `--customer-secondary-color-source`
250
+ - `--customer-secondary-color-light`
251
+ - `--customer-secondary-color-lighter`
252
+ - `--customer-secondary-color-dark`
253
+ - `--customer-secondary-color-darker`
254
+ - `--customer-secondary-color-text`
255
+ - `--customer-secondary-color-rgb`
256
+
257
+ ### Component Usage
258
+
259
+ Design system components consume the stable abstract variables, which fall back to the defaults when no customer context is provided:
260
+
261
+ ```css
262
+ /* From projects/ps-helix/src/lib/styles/themes/light.css */
263
+ --primary-color: var(--customer-primary-color, #0B0191);
264
+ --primary-color-light: var(--customer-primary-color-light, #0F02C4);
265
+ ```
266
+
267
+ ### Using in Your Custom Styles
268
+
269
+ Use the abstract variables (`--primary-color`, `--secondary-color`, ...) in your own components so you automatically inherit the accessibility-adjusted palette:
270
+
271
+ ```css
272
+ .my-custom-button {
273
+ background-color: var(--primary-color);
274
+ color: var(--primary-color-text);
275
+ }
276
+
277
+ .my-custom-button:hover {
278
+ background-color: var(--primary-color-light);
279
+ }
280
+ ```
281
+
282
+ For decorative brand elements that must match the literal brand color (e.g. a logo background), use the `-source` variable:
283
+
284
+ ```css
285
+ .brand-logo-surface {
286
+ background-color: var(--customer-primary-color-source);
287
+ }
288
+ ```
289
+
290
+ ## Dynamic Color Changes
291
+
292
+ Since the customer context service uses signals, you can change colors at runtime:
293
+
294
+ ```typescript
295
+ import { Component, inject } from '@angular/core';
296
+ import { ThemeService } from 'ps-helix';
297
+ import { AppThemeContextService } from './services/app-theme-context.service';
298
+
299
+ @Component({
300
+ selector: 'app-theme-switcher',
301
+ template: `
302
+ <button (click)="setRedTheme()">Red</button>
303
+ <button (click)="setBlueTheme()">Blue</button>
304
+ <button (click)="setGreenTheme()">Green</button>
305
+ `
306
+ })
307
+ export class ThemeSwitcherComponent {
308
+ private themeContext = inject(AppThemeContextService);
309
+ private themeService = inject(ThemeService);
310
+
311
+ setRedTheme() {
312
+ this.themeContext.setPrimaryColor('#DC2626');
313
+ this.themeContext.setSecondaryColor('#EF4444');
314
+ this.themeService.applyCustomerTheme();
315
+ }
316
+
317
+ setBlueTheme() {
318
+ this.themeContext.setPrimaryColor('#2563EB');
319
+ this.themeContext.setSecondaryColor('#3B82F6');
320
+ this.themeService.applyCustomerTheme();
321
+ }
322
+
323
+ setGreenTheme() {
324
+ this.themeContext.setPrimaryColor('#16A34A');
325
+ this.themeContext.setSecondaryColor('#22C55E');
326
+ this.themeService.applyCustomerTheme();
327
+ }
328
+ }
329
+ ```
330
+
331
+ ## Examples
332
+
333
+ ### Example 1: Brand Colors from Configuration
334
+
335
+ ```typescript
336
+ import { Injectable, signal } from '@angular/core';
337
+ import { environment } from '../environments/environment';
338
+
339
+ @Injectable({ providedIn: 'root' })
340
+ export class AppThemeContextService {
341
+ private primaryColorSignal = signal(environment.brandPrimaryColor);
342
+ private secondaryColorSignal = signal(environment.brandSecondaryColor);
343
+
344
+ primaryColor = this.primaryColorSignal.asReadonly();
345
+ secondaryColor = this.secondaryColorSignal.asReadonly();
346
+ }
347
+ ```
348
+
349
+ ### Example 2: User-Selected Theme
350
+
351
+ ```typescript
352
+ import { Injectable, signal, effect } from '@angular/core';
353
+
354
+ @Injectable({ providedIn: 'root' })
355
+ export class AppThemeContextService {
356
+ private primaryColorSignal = signal(this.loadFromStorage('primaryColor', '#0B0191'));
357
+ private secondaryColorSignal = signal(this.loadFromStorage('secondaryColor', '#5E5E5E'));
358
+
359
+ primaryColor = this.primaryColorSignal.asReadonly();
360
+ secondaryColor = this.secondaryColorSignal.asReadonly();
361
+
362
+ constructor() {
363
+ effect(() => {
364
+ localStorage.setItem('primaryColor', this.primaryColorSignal());
365
+ localStorage.setItem('secondaryColor', this.secondaryColorSignal());
366
+ });
367
+ }
368
+
369
+ setPrimaryColor(color: string) { this.primaryColorSignal.set(color); }
370
+ setSecondaryColor(color: string) { this.secondaryColorSignal.set(color); }
371
+
372
+ private loadFromStorage(key: string, defaultValue: string): string {
373
+ try { return localStorage.getItem(key) || defaultValue; }
374
+ catch { return defaultValue; }
375
+ }
376
+ }
377
+ ```
378
+
379
+ ### Example 3: Multi-Tenant Application
380
+
381
+ ```typescript
382
+ import { Injectable, signal } from '@angular/core';
383
+
384
+ interface TenantTheme {
385
+ primary: string;
386
+ secondary: string;
387
+ }
388
+
389
+ const TENANT_THEMES: Record<string, TenantTheme> = {
390
+ 'tenant-a': { primary: '#DC2626', secondary: '#EF4444' },
391
+ 'tenant-b': { primary: '#2563EB', secondary: '#3B82F6' },
392
+ 'tenant-c': { primary: '#16A34A', secondary: '#22C55E' }
393
+ };
394
+
395
+ @Injectable({ providedIn: 'root' })
396
+ export class AppThemeContextService {
397
+ private currentTenantId = signal<string>('tenant-a');
398
+ private primaryColorSignal = signal(TENANT_THEMES['tenant-a'].primary);
399
+ private secondaryColorSignal = signal(TENANT_THEMES['tenant-a'].secondary);
400
+
401
+ primaryColor = this.primaryColorSignal.asReadonly();
402
+ secondaryColor = this.secondaryColorSignal.asReadonly();
403
+
404
+ setTenant(tenantId: string) {
405
+ const theme = TENANT_THEMES[tenantId];
406
+ if (theme) {
407
+ this.currentTenantId.set(tenantId);
408
+ this.primaryColorSignal.set(theme.primary);
409
+ this.secondaryColorSignal.set(theme.secondary);
410
+ }
411
+ }
412
+ }
413
+ ```
414
+
415
+ ## Troubleshooting
416
+
417
+ ### Colors not applying
418
+
419
+ 1. Verify that your service is provided with the `CUSTOMER_CONTEXT_SERVICE` token in your app config.
420
+ 2. Confirm your service implements `primaryColor()` and `secondaryColor()` returning valid hex strings.
421
+ 3. `ThemeService` is silent by design; if a brand color looks different from the one you provided, it is because the WCAG safeguard adjusted it. Read the adjusted color via `getComputedStyle(document.documentElement).getPropertyValue('--customer-primary-color')` and the original via `--customer-primary-color-source`.
422
+
423
+ ### Default colors showing instead of custom colors
424
+
425
+ - The context service is not provided, or
426
+ - `primaryColor()` / `secondaryColor()` return an invalid hex string.
427
+
428
+ ### Colors not updating dynamically
429
+
430
+ 1. Make sure you call `this.themeService.applyCustomerTheme()` after changing a color.
431
+ 2. Verify that your signals are actually updating.
432
+ 3. Confirm `ThemeService` is injected in the component driving the change.
433
+
434
+ ## Best Practices
435
+
436
+ 1. Use signals in your customer context service for reactivity.
437
+ 2. Provide valid hex colors (`#RRGGBB`).
438
+ 3. Trust the WCAG safeguard; do not hand-tune base colors for contrast.
439
+ 4. For decorative brand surfaces, use `--customer-*-color-source`. For every other UI use case, use the abstract variables (`--primary-color`, ...).
440
+ 5. Persist user preferences in `localStorage` when relevant.
441
+ 6. Document brand color choices alongside your app's style guide.
442
+
443
+ ## Related Documentation
444
+
445
+ - [Component Documentation](./README.md)
446
+ - [Translation Guide](./lib/services/translation/README.md)