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/README.md +1188 -1188
- package/THEME.md +446 -446
- package/fesm2022/ps-helix.mjs +1949 -1012
- package/fesm2022/ps-helix.mjs.map +1 -1
- package/package.json +6 -5
- package/src/lib/styles/base/global.css +48 -32
- package/src/lib/styles/themes/dark.css +200 -200
- package/src/lib/styles/themes/light.css +278 -278
- package/src/lib/styles/tokens/breakpoints.tokens.css +20 -0
- package/src/lib/styles/tokens/sizing.tokens.css +94 -94
- package/src/lib/styles/tokens/spacing.tokens.css +27 -27
- package/src/lib/styles/utilities/animations.utils.css +157 -157
- package/src/lib/styles/utilities/colors.utils.css +223 -223
- package/src/lib/styles/utilities/focus.utils.css +56 -62
- package/src/lib/styles/utilities/layout.utils.css +135 -135
- package/src/lib/styles/utilities/responsive.utils.css +219 -226
- package/src/lib/styles/utilities/spacing.utils.css +150 -150
- package/src/lib/styles/utilities/typography.utils.css +36 -36
- package/styles.css +34 -33
- package/types/ps-helix.d.ts +291 -67
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)
|